@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.
Files changed (93) hide show
  1. package/dist/account/cli.d.ts +23 -0
  2. package/dist/account/cli.js +249 -0
  3. package/dist/account/cloud-contract.d.ts +39 -0
  4. package/dist/account/cloud-contract.js +134 -0
  5. package/dist/account/cloud-types.d.ts +47 -0
  6. package/dist/account/cloud-types.js +1 -0
  7. package/dist/account/feature-service.d.ts +47 -0
  8. package/dist/account/feature-service.js +184 -0
  9. package/dist/account/http.d.ts +17 -0
  10. package/dist/account/http.js +158 -0
  11. package/dist/account/links.d.ts +13 -0
  12. package/dist/account/links.js +137 -0
  13. package/dist/account/manager.d.ts +96 -0
  14. package/dist/account/manager.js +976 -0
  15. package/dist/account/native-lock.d.ts +6 -0
  16. package/dist/account/native-lock.js +26 -0
  17. package/dist/account/native-store.d.ts +32 -0
  18. package/dist/account/native-store.js +162 -0
  19. package/dist/account/profile.d.ts +4 -0
  20. package/dist/account/profile.js +46 -0
  21. package/dist/account/store.d.ts +34 -0
  22. package/dist/account/store.js +118 -0
  23. package/dist/account/types.d.ts +138 -0
  24. package/dist/account/types.js +2 -0
  25. package/dist/desktop-web/http-api.d.ts +4 -0
  26. package/dist/desktop-web/http-api.js +12 -4
  27. package/dist/environment-identity.d.ts +1 -1
  28. package/dist/environment-identity.js +2 -1
  29. package/dist/headless-worker-env.d.ts +2 -0
  30. package/dist/headless-worker-env.js +15 -0
  31. package/dist/hub/approval-lease.js +6 -1
  32. package/dist/hub/configuration.d.ts +11 -0
  33. package/dist/hub/configuration.js +34 -1
  34. package/dist/hub/model-parameter-metadata.d.ts +26 -0
  35. package/dist/hub/model-parameter-metadata.js +92 -0
  36. package/dist/hub/run-replay.js +1 -1
  37. package/dist/hub/session-management.js +1 -1
  38. package/dist/index.account.d.ts +12 -0
  39. package/dist/index.account.js +12 -0
  40. package/dist/index.mobile-remote.d.ts +2 -0
  41. package/dist/index.mobile-remote.js +1 -0
  42. package/dist/index.worker.d.ts +1 -0
  43. package/dist/index.worker.js +1 -0
  44. package/dist/mobile-remote/account-relay-access.d.ts +59 -0
  45. package/dist/mobile-remote/account-relay-access.js +174 -0
  46. package/dist/mobile-remote/approval-clock.d.ts +3 -0
  47. package/dist/mobile-remote/approval-clock.js +15 -0
  48. package/dist/mobile-remote/codex-room-agent.d.ts +6 -0
  49. package/dist/mobile-remote/codex-room-agent.js +17 -1
  50. package/dist/mobile-remote/mobile-chat-turn.d.ts +4 -0
  51. package/dist/mobile-remote/mobile-chat-turn.js +3 -0
  52. package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
  53. package/dist/mobile-remote/mobile-upload-service.d.ts +9 -0
  54. package/dist/mobile-remote/mobile-upload-service.js +25 -0
  55. package/dist/mobile-remote/remote-host-manager.d.ts +21 -4
  56. package/dist/mobile-remote/remote-host-manager.js +142 -25
  57. package/dist/mobile-remote/resident-agent.d.ts +14 -0
  58. package/dist/mobile-remote/resident-agent.js +38 -11
  59. package/dist/mobile-remote/room-history.d.ts +7 -0
  60. package/dist/mobile-remote/room-history.js +56 -1
  61. package/dist/mobile-remote/room-manager.d.ts +216 -9
  62. package/dist/mobile-remote/room-manager.js +700 -65
  63. package/dist/mobile-remote/room-process-stop.d.ts +15 -0
  64. package/dist/mobile-remote/room-process-stop.js +41 -0
  65. package/dist/mobile-remote/trusted-device-store.d.ts +12 -0
  66. package/dist/mobile-remote/trusted-device-store.js +54 -1
  67. package/dist/mobile-remote/turn-queue.d.ts +183 -0
  68. package/dist/mobile-remote/turn-queue.js +422 -0
  69. package/dist/mobile-remote/types.d.ts +1 -1
  70. package/dist/panels/agent-task-host.js +6 -12
  71. package/dist/panels/agent-task-service.d.ts +2 -0
  72. package/dist/panels/agent-task-service.js +7 -0
  73. package/dist/panels/browser-capture.d.ts +1 -15
  74. package/dist/panels/browser-capture.js +1 -35
  75. package/dist/panels/directory-picker.d.ts +48 -0
  76. package/dist/panels/directory-picker.js +247 -0
  77. package/dist/panels/http.d.ts +15 -15
  78. package/dist/panels/management-http.d.ts +9 -9
  79. package/dist/panels/process-service.d.ts +5 -4
  80. package/dist/panels/process-service.js +54 -16
  81. package/dist/panels/runtime.js +148 -30
  82. package/dist/panels/tool-jobs-storage.d.ts +4 -0
  83. package/dist/panels/tool-jobs-storage.js +111 -34
  84. package/dist/panels/types.d.ts +1 -97
  85. package/dist/remote-relay/connector.d.ts +8 -0
  86. package/dist/remote-relay/connector.js +29 -2
  87. package/dist/remote-relay/protocol.d.ts +21 -0
  88. package/dist/remote-relay/protocol.js +31 -0
  89. package/dist/serve/cli.js +6 -0
  90. package/dist/serve/headless-server.js +24 -19
  91. package/dist/sessions-service.d.ts +4 -0
  92. package/dist/sessions-service.js +14 -1
  93. 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
+ }
@@ -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("./types.js").PanelSnapshot>;
21
- packageHistory(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelPackageHistory>;
22
- previewRestore(context: import("./types.js").PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("./types.js").PanelPackageRestoreReview>;
23
- cancelRestore(context: import("./types.js").PanelOperationContext, token: unknown): Promise<{
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("./types.js").PanelOperationContext, token: unknown): Promise<{
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("./types.js").PanelOperationContext, expected?: number) => Promise<void>;
31
- discover(context: import("./types.js").PanelOperationContext, input: unknown): Promise<import("./types.js").PanelDiscovery>;
32
- preview: (context: import("./types.js").PanelOperationContext, input: unknown) => Promise<import("./types.js").PanelReview>;
33
- previewUpdate(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelReview>;
34
- previewProjectSource: (context: import("./types.js").PanelOperationContext, input: import("@cjhyy/code-shell-core").PanelAppSourceInput, expected?: {
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("./types.js").PanelProjectReview>;
38
- previewProjectUpdate(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelProjectReview>;
39
- install(context: import("./types.js").PanelOperationContext, token: unknown, bind?: boolean, approval?: {
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("./types.js").PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("./types.js").PanelSnapshot>;
48
- remove(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<{
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("./types.js").PanelSnapshot>;
12
- packageHistory(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelPackageHistory>;
13
- previewRestore(context: PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("./types.js").PanelPackageRestoreReview>;
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("./types.js").PanelDiscovery>;
23
- preview: (context: PanelOperationContext, input: unknown) => Promise<import("./types.js").PanelReview>;
24
- previewUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelReview>;
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("./types.js").PanelProjectReview>;
29
- previewProjectUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelProjectReview>;
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("./types.js").PanelSnapshot>;
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
- * Desktop delegates durable approval lifetime to its Host hooks. Web uses guest
30
- * to isolate each authenticated instance; guest approvals never use the
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
- /** Desktop can use installed-app process permission and trusted-project binding as consent. */
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
- // Desktop already checks installed-app process permission and a trusted,
466
- // bound project in authorize(). Web keeps its per-guest confirmation path.
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
- const allowed = await this.options.confirmExecution({
475
- guestId: owner.guestId,
476
- appId: owner.appId,
477
- appTitle: owner.appTitle,
478
- executable: executable.name,
479
- executablePath: executable.path,
480
- });
481
- await this.authorize(owner, epoch);
482
- if (!allowed)
483
- throw new Error(`User denied running ${executable.name}`);
484
- if (!guestScoped) {
485
- await this.options.rememberExecutionApproval?.(scope).catch(() => undefined);
486
- await this.authorize(owner, epoch);
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
- // Cancellation also waits for the process group, not just its leader.
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) {