@cjhyy/code-shell-server 0.9.22 → 0.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +45 -1
  2. package/dist/bin/code-shell-settings-recovery.d.ts +2 -0
  3. package/dist/bin/code-shell-settings-recovery.js +4 -0
  4. package/dist/desktop-web/http-api.js +14 -0
  5. package/dist/environment-identity.d.ts +4 -0
  6. package/dist/environment-identity.js +67 -0
  7. package/dist/hub/auth-store.d.ts +2 -0
  8. package/dist/hub/auth-store.js +7 -0
  9. package/dist/index.auth.d.ts +3 -0
  10. package/dist/index.auth.js +2 -0
  11. package/dist/index.links.d.ts +1 -0
  12. package/dist/index.links.js +1 -0
  13. package/dist/index.panels.d.ts +7 -0
  14. package/dist/index.panels.js +7 -0
  15. package/dist/index.remote-relay.d.ts +5 -0
  16. package/dist/index.remote-relay.js +6 -0
  17. package/dist/index.serve.d.ts +2 -0
  18. package/dist/index.serve.js +2 -0
  19. package/dist/links/http.d.ts +3 -0
  20. package/dist/links/http.js +11 -2
  21. package/dist/links/remote-configuration.d.ts +4 -0
  22. package/dist/links/remote-configuration.js +32 -0
  23. package/dist/links/service.d.ts +6 -1
  24. package/dist/links/service.js +241 -19
  25. package/dist/mobile-remote/mobile-upload-service.d.ts +4 -0
  26. package/dist/mobile-remote/mobile-upload-service.js +69 -11
  27. package/dist/mobile-remote/remote-host-manager.d.ts +10 -5
  28. package/dist/mobile-remote/remote-host-manager.js +53 -5
  29. package/dist/panels/agent-task-service.js +9 -2
  30. package/dist/panels/automations.d.ts +49 -0
  31. package/dist/panels/automations.js +134 -0
  32. package/dist/panels/bridge-contract.d.ts +5 -3
  33. package/dist/panels/bridge-contract.js +11 -1
  34. package/dist/panels/directory-bookmarks.d.ts +9 -1
  35. package/dist/panels/directory-bookmarks.js +64 -5
  36. package/dist/panels/execution-gate.d.ts +26 -0
  37. package/dist/panels/execution-gate.js +70 -0
  38. package/dist/panels/http.d.ts +26 -2
  39. package/dist/panels/http.js +7 -1
  40. package/dist/panels/hub-automations.d.ts +31 -0
  41. package/dist/panels/hub-automations.js +352 -0
  42. package/dist/panels/management-http.d.ts +20 -2
  43. package/dist/panels/management-http.js +15 -2
  44. package/dist/panels/management.d.ts +26 -4
  45. package/dist/panels/management.js +382 -39
  46. package/dist/panels/package-mutation.d.ts +8 -0
  47. package/dist/panels/package-mutation.js +23 -0
  48. package/dist/panels/process-service.d.ts +3 -0
  49. package/dist/panels/process-service.js +77 -4
  50. package/dist/panels/resources/clone.d.ts +13 -0
  51. package/dist/panels/resources/clone.js +99 -0
  52. package/dist/panels/resources/http.d.ts +11 -0
  53. package/dist/panels/resources/http.js +74 -0
  54. package/dist/panels/resources/references.d.ts +5 -0
  55. package/dist/panels/resources/references.js +26 -0
  56. package/dist/panels/resources/service.js +63 -27
  57. package/dist/panels/runtime-services.js +16 -3
  58. package/dist/panels/runtime.d.ts +16 -4
  59. package/dist/panels/runtime.js +522 -64
  60. package/dist/panels/shared-tool-jobs.d.ts +40 -0
  61. package/dist/panels/shared-tool-jobs.js +37 -0
  62. package/dist/panels/storage-store.d.ts +21 -0
  63. package/dist/panels/storage-store.js +43 -0
  64. package/dist/panels/task-cookie-host.d.ts +26 -0
  65. package/dist/panels/task-cookie-host.js +145 -0
  66. package/dist/panels/task-cookies.d.ts +51 -0
  67. package/dist/panels/task-cookies.js +183 -0
  68. package/dist/panels/tool-executor.d.ts +21 -5
  69. package/dist/panels/tool-executor.js +171 -30
  70. package/dist/panels/tool-jobs-storage.d.ts +2 -0
  71. package/dist/panels/tool-jobs-storage.js +34 -0
  72. package/dist/panels/tool-jobs.d.ts +49 -3
  73. package/dist/panels/tool-jobs.js +270 -15
  74. package/dist/panels/types.d.ts +43 -0
  75. package/dist/project-runtime/backup.d.ts +26 -0
  76. package/dist/project-runtime/backup.js +500 -0
  77. package/dist/project-runtime/control-server.d.ts +4 -0
  78. package/dist/project-runtime/control-server.js +19 -1
  79. package/dist/project-runtime/docker-provider.d.ts +4 -0
  80. package/dist/project-runtime/docker-provider.js +16 -1
  81. package/dist/project-runtime/managed-entry.js +4 -0
  82. package/dist/project-runtime/registry.d.ts +2 -0
  83. package/dist/project-runtime/registry.js +15 -0
  84. package/dist/project-runtime/seccomp.d.ts +8 -0
  85. package/dist/project-runtime/seccomp.js +72 -0
  86. package/dist/project-runtime/types.d.ts +2 -0
  87. package/dist/remote-relay/byte-stream.d.ts +9 -0
  88. package/dist/remote-relay/byte-stream.js +71 -0
  89. package/dist/remote-relay/connector.d.ts +27 -0
  90. package/dist/remote-relay/connector.js +197 -0
  91. package/dist/remote-relay/local-stream.d.ts +10 -0
  92. package/dist/remote-relay/local-stream.js +120 -0
  93. package/dist/remote-relay/protocol.d.ts +39 -0
  94. package/dist/remote-relay/protocol.js +64 -0
  95. package/dist/serve/automation-worker.d.ts +16 -0
  96. package/dist/serve/automation-worker.js +126 -0
  97. package/dist/serve/cli.d.ts +3 -0
  98. package/dist/serve/cli.js +17 -0
  99. package/dist/serve/headless-server.d.ts +3 -0
  100. package/dist/serve/headless-server.js +140 -10
  101. package/dist/serve/settings-recovery-cli.d.ts +29 -0
  102. package/dist/serve/settings-recovery-cli.js +128 -0
  103. package/dist/session-http-responses.d.ts +7 -0
  104. package/dist/session-http-responses.js +40 -0
  105. package/dist/sessions-service.d.ts +6 -2
  106. package/dist/sessions-service.js +23 -4
  107. package/dist/worker-bridge-core.d.ts +5 -0
  108. package/dist/worker-bridge-core.js +23 -0
  109. package/package.json +15 -6
@@ -1,20 +1,44 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { type PanelManagementHttpOptions } from "./management-http.js";
3
+ import type { PanelDirectoryAuthorizer } from "./directory-bookmarks.js";
4
+ import type { SharedPanelToolHost } from "./shared-tool-jobs.js";
5
+ import type { PanelAutomationHost } from "./automations.js";
3
6
  import { type PanelAgentTaskHostOptions } from "./agent-task-host.js";
4
7
  export declare function createPanelHttp(options: PanelManagementHttpOptions & {
5
8
  dataDir: string;
6
9
  host: "hub" | "desktop";
7
10
  publicPathPrefix?: string;
8
11
  agentTaskOptions?: PanelAgentTaskHostOptions;
12
+ sharedToolJobs?: SharedPanelToolHost;
13
+ automations?: PanelAutomationHost;
14
+ authorizePanelDirectory?: PanelDirectoryAuthorizer;
9
15
  }): {
10
16
  service: {
11
- snapshot: () => Promise<import("./types.js").PanelSnapshot>;
17
+ snapshot: (appId?: string) => Promise<import("./types.js").PanelSnapshot>;
18
+ packageHistory(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelPackageHistory>;
19
+ previewRestore(context: import("./types.js").PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<import("./types.js").PanelPackageRestoreReview>;
20
+ cancelRestore(context: import("./types.js").PanelOperationContext, token: unknown): Promise<{
21
+ cancelled: boolean;
22
+ }>;
23
+ restore(context: import("./types.js").PanelOperationContext, token: unknown): Promise<{
24
+ id: string;
25
+ packageDigest: string;
26
+ }>;
12
27
  assertAuthorized: (context: import("./types.js").PanelOperationContext, expected?: number) => Promise<void>;
13
28
  discover(context: import("./types.js").PanelOperationContext, input: unknown): Promise<import("./types.js").PanelDiscovery>;
14
29
  preview: (context: import("./types.js").PanelOperationContext, input: unknown) => Promise<import("./types.js").PanelReview>;
15
30
  previewUpdate(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelReview>;
16
- install(context: import("./types.js").PanelOperationContext, token: unknown, bind?: boolean): Promise<{
31
+ previewProjectSource: (context: import("./types.js").PanelOperationContext, input: import("@cjhyy/code-shell-core").PanelAppSourceInput, expected?: {
32
+ id: string;
33
+ revision: string;
34
+ }) => Promise<import("./types.js").PanelProjectReview>;
35
+ previewProjectUpdate(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelProjectReview>;
36
+ install(context: import("./types.js").PanelOperationContext, token: unknown, bind?: boolean, approval?: {
37
+ overwrite?: boolean;
38
+ expectedId?: string;
39
+ }): Promise<{
17
40
  id: string;
41
+ packageDigest: string | undefined;
18
42
  }>;
19
43
  binding(context: import("./types.js").PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("./types.js").PanelSnapshot>;
20
44
  remove(context: import("./types.js").PanelOperationContext, id: unknown, expected: unknown): Promise<{
@@ -2,9 +2,14 @@ import { createPanelManagementHttp } from "./management-http.js";
2
2
  import { createPanelRuntime, panelWebCompatibility } from "./runtime.js";
3
3
  import { createPanelAgentTaskHost } from "./agent-task-host.js";
4
4
  export function createPanelHttp(options) {
5
+ // Embedded Desktop hosts opt in once their native coordinator and protocol
6
+ // implement the same selection. The current Desktop composition does so.
7
+ const projectPackages = options.projectPackages ?? options.host === "hub";
5
8
  const management = createPanelManagementHttp({
6
9
  ...options,
7
- compatibility: options.compatibility ?? panelWebCompatibility,
10
+ projectPackages,
11
+ compatibility: options.compatibility ??
12
+ ((app) => panelWebCompatibility(app, { automations: !!options.automations })),
8
13
  onChanged: async (id, kind) => {
9
14
  await runtime?.invalidate(id);
10
15
  await options.onChanged?.(id, kind);
@@ -12,6 +17,7 @@ export function createPanelHttp(options) {
12
17
  });
13
18
  const runtime = createPanelRuntime({
14
19
  ...options,
20
+ projectPackages,
15
21
  snapshot: management.service.snapshot,
16
22
  createAgentTasks: (hooks) => createPanelAgentTaskHost({ ...options.agentTaskOptions, ...hooks }),
17
23
  });
@@ -0,0 +1,31 @@
1
+ import { type CronJob, type CronJobLifecycleEvent, type CronRunRequest } from "@cjhyy/code-shell-core/internal";
2
+ import { type PanelAutomationHost } from "./automations.js";
3
+ /** A transport lost the outcome after dispatch. Never automatically replay it. */
4
+ export declare class HubAutomationUncertainError extends Error {
5
+ }
6
+ export declare class HubAutomationCancelledError extends Error {
7
+ }
8
+ export interface HubPanelAutomationOptions {
9
+ cwd: string;
10
+ /** Canonical binding root when the workspace is a linked worktree. */
11
+ bindingCwd?: string;
12
+ dataDir: string;
13
+ sessionRootDir: string;
14
+ /** Must check the current project binding, permission and exact package revision.
15
+ * Throw to stop a task whose source is no longer authorized. Never uses a page grant.
16
+ */
17
+ assertExecutable(job: Readonly<CronJob>): Promise<void>;
18
+ /** Host-owned runner; must forward policy and cooperate with abort before resolving.
19
+ * The Hub composition must reserve the bound Session against interactive runs.
20
+ */
21
+ execute(request: CronRunRequest): Promise<void>;
22
+ onJobEvent?(event: CronJobLifecycleEvent): void;
23
+ }
24
+ /** Project-owned scheduling. The Hub supplies its shared Worker, approval
25
+ * policy and Session reservation; embedded hosts may supply equivalent runners.
26
+ */
27
+ export declare function createHubPanelAutomationHost(options: HubPanelAutomationOptions): {
28
+ host: PanelAutomationHost;
29
+ close: () => Promise<void>;
30
+ activeCount: () => number;
31
+ };
@@ -0,0 +1,352 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync, lstatSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { CronScheduler, CronStore, lockSync, resolveWritePolicy, } from "@cjhyy/code-shell-core/internal";
5
+ import { panelExecutionGate } from "./execution-gate.js";
6
+ import { readHubSessionState } from "../hub/session-management.js";
7
+ import { panelAutomationCreationKey, panelAutomationRevision, assertPanelAutomationRevision, PanelAutomationConflictError, parsePanelAutomationCall, } from "./automations.js";
8
+ /** A transport lost the outcome after dispatch. Never automatically replay it. */
9
+ export class HubAutomationUncertainError extends Error {
10
+ }
11
+ export class HubAutomationCancelledError extends Error {
12
+ }
13
+ function summary(job) {
14
+ return {
15
+ id: job.id,
16
+ revision: panelAutomationRevision(job),
17
+ name: job.name,
18
+ schedule: job.schedule,
19
+ prompt: job.prompt,
20
+ enabled: job.enabled,
21
+ cwd: job.cwd ?? null,
22
+ timezone: job.timezone ?? null,
23
+ permissionLevel: job.permissionLevel ?? null,
24
+ resumeSessionId: job.resumeSessionId ?? null,
25
+ runCount: job.runCount,
26
+ createdAt: job.createdAt,
27
+ lastRun: job.lastRun ?? null,
28
+ nextRun: job.nextRun ?? null,
29
+ disabledReason: job.disabledReason ?? null,
30
+ panelSource: job.panelSource ? { ...job.panelSource } : null,
31
+ lastExecution: job.lastExecution ? { ...job.lastExecution } : null,
32
+ };
33
+ }
34
+ /** Project-owned scheduling. The Hub supplies its shared Worker, approval
35
+ * policy and Session reservation; embedded hosts may supply equivalent runners.
36
+ */
37
+ export function createHubPanelAutomationHost(options) {
38
+ const cwd = resolve(options.cwd);
39
+ const directory = join(options.dataDir, "panel-automations");
40
+ const records = join(directory, "records");
41
+ const owner = join(directory, "owner");
42
+ for (const path of [directory, records, owner]) {
43
+ mkdirSync(path, { recursive: true, mode: 0o700 });
44
+ const info = lstatSync(path);
45
+ if (!info.isDirectory() || info.isSymbolicLink())
46
+ throw Error("Unsafe automation directory");
47
+ }
48
+ // The lifetime lease and bounded CronStore transaction lock use different
49
+ // directories; neither nests a second acquisition of the same lock.
50
+ let closed = false, compromised = false;
51
+ const leaseState = {};
52
+ const unlock = lockSync(owner, {
53
+ stale: 30000,
54
+ update: 10000,
55
+ retries: 0,
56
+ onCompromised: () => {
57
+ compromised = true;
58
+ leaseState.lost?.();
59
+ },
60
+ });
61
+ const identities = [directory, records, owner, `${owner}.lock`].map((path) => ({
62
+ path,
63
+ info: lstatSync(path),
64
+ }));
65
+ const store = new CronStore(join(records, "cron.json"), { strictRead: true });
66
+ const scheduler = new CronScheduler(store);
67
+ const active = new Set();
68
+ const assertStorage = () => {
69
+ if (compromised)
70
+ throw Error("Cloud automation owner is unavailable");
71
+ for (const { path, info } of identities) {
72
+ const current = lstatSync(path);
73
+ if (!current.isDirectory() ||
74
+ current.isSymbolicLink() ||
75
+ current.dev !== info.dev ||
76
+ current.ino !== info.ino)
77
+ throw Error("Cloud automation storage or owner directory changed");
78
+ }
79
+ };
80
+ const assertOwner = () => {
81
+ if (closed)
82
+ throw Error("Cloud automation owner is unavailable");
83
+ assertStorage();
84
+ };
85
+ const assertSession = (sessionId) => {
86
+ const state = readHubSessionState(options.sessionRootDir, sessionId);
87
+ if (resolve(state.cwd) !== cwd)
88
+ throw Error("Automation task is outside this project");
89
+ };
90
+ scheduler.setJobEventListener((event) => options.onJobEvent?.(event));
91
+ scheduler.setExecutor(async (live, signal) => {
92
+ active.add(live.id);
93
+ const job = structuredClone(live);
94
+ let releaseExecution;
95
+ try {
96
+ assertOwner();
97
+ if (job.panelSource) {
98
+ releaseExecution = panelExecutionGate.enter({
99
+ appId: job.panelSource.appId,
100
+ projectPath: options.bindingCwd ?? cwd,
101
+ });
102
+ }
103
+ try {
104
+ if (job.cwd !== cwd || !job.resumeSessionId || !job.panelSource)
105
+ throw Error("Automation binding is incomplete");
106
+ assertSession(job.resumeSessionId);
107
+ await options.assertExecutable(job);
108
+ }
109
+ catch (error) {
110
+ if (signal.aborted || closed || compromised)
111
+ return;
112
+ assertOwner();
113
+ const reason = error instanceof Error ? error.message : "Automation authorization unavailable";
114
+ scheduler.disableWithReason(job.id, reason);
115
+ return { stoppedReason: reason };
116
+ }
117
+ assertOwner();
118
+ if (signal.aborted)
119
+ return;
120
+ // Re-read task ownership after asynchronous package verification. A
121
+ // deleted/moved Session must not be recreated by an unattended run.
122
+ assertSession(job.resumeSessionId);
123
+ // A pending verification must not execute a deleted/replaced definition.
124
+ // A strict reload also refuses corrupt persistence before side effects.
125
+ const current = store.load().find((candidate) => candidate.id === job.id);
126
+ if (!current ||
127
+ current.createdAt !== job.createdAt ||
128
+ current.prompt !== job.prompt ||
129
+ current.resumeSessionId !== job.resumeSessionId ||
130
+ current.cwd !== job.cwd ||
131
+ current.panelSource?.appId !== job.panelSource?.appId ||
132
+ current.panelSource?.revision !== job.panelSource?.revision ||
133
+ current.permissionLevel !== job.permissionLevel)
134
+ throw Error("Automation changed while preparing execution");
135
+ if (current.lastExecution?.status === "running")
136
+ throw Error("Previous automation outcome is unknown; restart to reconcile before retrying");
137
+ const receipt = {
138
+ id: randomUUID(),
139
+ status: "running",
140
+ startedAt: Date.now(),
141
+ };
142
+ // Persist admission before any Worker frame or model/tool side effect.
143
+ store.mutate((jobs) => {
144
+ const latest = jobs.find((value) => value.id === job.id);
145
+ if (!latest ||
146
+ latest.createdAt !== job.createdAt ||
147
+ latest.prompt !== job.prompt ||
148
+ latest.resumeSessionId !== job.resumeSessionId ||
149
+ latest.cwd !== job.cwd ||
150
+ latest.permissionLevel !== job.permissionLevel ||
151
+ latest.panelSource?.appId !== job.panelSource?.appId ||
152
+ latest.panelSource?.revision !== job.panelSource?.revision ||
153
+ latest.lastExecution?.status === "running")
154
+ throw Error("Automation changed or has an unresolved execution");
155
+ return {
156
+ jobs: jobs.map((value) => value.id === job.id
157
+ ? { ...value, lastExecution: receipt, lastRunId: receipt.id }
158
+ : value),
159
+ result: null,
160
+ };
161
+ });
162
+ job.lastRunId = live.lastRunId = receipt.id;
163
+ let failure;
164
+ let failed = false;
165
+ try {
166
+ await options.execute({
167
+ job,
168
+ prompt: job.prompt,
169
+ signal,
170
+ ...resolveWritePolicy(job.permissionLevel),
171
+ });
172
+ }
173
+ catch (error) {
174
+ failure = error;
175
+ failed = true;
176
+ }
177
+ const uncertain = failure instanceof HubAutomationUncertainError;
178
+ const status = uncertain
179
+ ? "interrupted"
180
+ : signal.aborted || failure instanceof HubAutomationCancelledError
181
+ ? "cancelled"
182
+ : failed
183
+ ? "failed"
184
+ : "completed";
185
+ const detail = failed
186
+ ? String(failure instanceof Error ? failure.message : failure)
187
+ .replaceAll("\0", "")
188
+ .slice(0, 2000)
189
+ : undefined;
190
+ assertStorage();
191
+ // A deleted task stays deleted; a replacement never receives this run's receipt.
192
+ // If saving fails, the running checkpoint remains and later fires refuse it.
193
+ store.mutate((jobs) => ({
194
+ jobs: jobs.map((value) => value.id === job.id && value.lastExecution?.id === receipt.id
195
+ ? {
196
+ ...value,
197
+ lastExecution: {
198
+ ...receipt,
199
+ status,
200
+ finishedAt: Math.max(Date.now(), receipt.startedAt),
201
+ ...(detail ? { detail } : {}),
202
+ },
203
+ ...(uncertain
204
+ ? { enabled: false, disabledReason: detail ?? "Execution outcome is unknown" }
205
+ : {}),
206
+ }
207
+ : value),
208
+ result: null,
209
+ }));
210
+ scheduler.loadJobs();
211
+ if (failure instanceof HubAutomationCancelledError)
212
+ return { cancelled: true };
213
+ if (failed)
214
+ throw failure;
215
+ }
216
+ finally {
217
+ releaseExecution?.();
218
+ active.delete(job.id);
219
+ }
220
+ });
221
+ let closing;
222
+ const close = () => {
223
+ if (closing)
224
+ return closing;
225
+ closed = true;
226
+ clearInterval(leaseState.timer);
227
+ scheduler.setExecutionEnabled(false);
228
+ scheduler.stopAll();
229
+ closing = Promise.all([...active].map((id) => scheduler.abort(id))).then(() => {
230
+ if (!compromised)
231
+ unlock();
232
+ });
233
+ return closing;
234
+ };
235
+ leaseState.lost = () => {
236
+ void close().catch(() => { });
237
+ };
238
+ try {
239
+ // Validate ownership before loadJobs can arm timers or persist misfire stats.
240
+ if (store.load().some((job) => job.cwd !== cwd))
241
+ throw Error("Automation store belongs to another project");
242
+ if (store.load().some((job) => job.lastExecution?.status === "running")) {
243
+ store.mutate((jobs) => ({
244
+ jobs: jobs.map((job) => job.lastExecution?.status === "running"
245
+ ? {
246
+ ...job,
247
+ enabled: false,
248
+ disabledReason: "Previous execution was interrupted; inspect its results before resuming",
249
+ lastExecution: {
250
+ ...job.lastExecution,
251
+ status: "interrupted",
252
+ finishedAt: Math.max(Date.now(), job.lastExecution.startedAt),
253
+ detail: "Host stopped before recording a terminal outcome",
254
+ },
255
+ }
256
+ : job),
257
+ result: null,
258
+ }));
259
+ }
260
+ scheduler.loadJobs();
261
+ }
262
+ catch (error) {
263
+ scheduler.stopAll();
264
+ unlock();
265
+ throw error;
266
+ }
267
+ leaseState.timer = setInterval(() => {
268
+ try {
269
+ assertOwner();
270
+ }
271
+ catch {
272
+ compromised = true;
273
+ leaseState.lost?.();
274
+ }
275
+ }, 1000);
276
+ leaseState.timer.unref?.();
277
+ const host = {
278
+ conditionalMutations: true,
279
+ async call(scope, method, params) {
280
+ assertOwner();
281
+ const operation = parsePanelAutomationCall(method, params);
282
+ if (resolve(scope.cwd) !== cwd || !/^[a-z0-9][a-z0-9-]{0,127}$/.test(scope.appId))
283
+ throw Error("Automation scope does not match this project");
284
+ if (!(await scope.isAuthorized()))
285
+ throw Error("Panel automation authorization expired");
286
+ assertSession(scope.sessionId);
287
+ assertOwner();
288
+ if (!(await scope.isAuthorized()))
289
+ throw Error("Panel automation authorization expired");
290
+ assertOwner();
291
+ assertSession(scope.sessionId);
292
+ scheduler.loadJobs();
293
+ const owns = (job) => job.cwd === cwd &&
294
+ job.resumeSessionId === scope.sessionId &&
295
+ job.panelSource?.appId === scope.appId;
296
+ const guard = (job) => {
297
+ if (!owns(job))
298
+ throw Error("Automation task is not available in this Panel project");
299
+ if (!["pause", "delete"].includes(operation.action) &&
300
+ job.panelSource?.revision !== scope.revision)
301
+ throw Error("Automation uses a different Panel revision; restore its package before continuing");
302
+ };
303
+ if (operation.action === "list")
304
+ return { automations: scheduler.list().filter(owns).map(summary) };
305
+ if (operation.action === "create") {
306
+ if (typeof scope.revision !== "string" || !/^[a-f0-9]{64}$/.test(scope.revision))
307
+ throw Error("Automation requires a reviewed package revision");
308
+ return summary(scheduler.create(operation.input.name, operation.input.schedule, operation.input.prompt, {
309
+ cwd,
310
+ resumeSessionId: scope.sessionId,
311
+ timezone: operation.input.timezone,
312
+ permissionLevel: "full",
313
+ panelSource: { appId: scope.appId, revision: scope.revision },
314
+ ...(operation.key
315
+ ? {
316
+ creationKey: panelAutomationCreationKey(scope.appId, cwd, scope.sessionId, operation.key),
317
+ }
318
+ : {}),
319
+ }));
320
+ }
321
+ const job = scheduler.get(operation.id);
322
+ if (!job) {
323
+ if (operation.expectedRevision)
324
+ return { ok: false, conflict: true };
325
+ throw Error("Automation task is unavailable");
326
+ }
327
+ guard(job);
328
+ const check = (current) => {
329
+ guard(current);
330
+ assertPanelAutomationRevision(current, operation.expectedRevision);
331
+ };
332
+ try {
333
+ if (operation.action === "update") {
334
+ const updated = scheduler.update(job.id, operation.patch, check);
335
+ if (operation.expectedRevision)
336
+ return updated
337
+ ? { ok: true, automation: summary(updated) }
338
+ : { ok: false, conflict: true };
339
+ return updated ? summary(updated) : null;
340
+ }
341
+ const ok = scheduler[operation.action](job.id, check);
342
+ return operation.expectedRevision && !ok ? { ok: false, conflict: true } : { ok };
343
+ }
344
+ catch (error) {
345
+ if (operation.expectedRevision && error instanceof PanelAutomationConflictError)
346
+ return { ok: false, conflict: true };
347
+ throw error;
348
+ }
349
+ },
350
+ };
351
+ return { host, close, activeCount: () => active.size };
352
+ }
@@ -8,13 +8,31 @@ 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: () => Promise<import("./types.js").PanelSnapshot>;
11
+ snapshot: (appId?: string) => 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>;
14
+ cancelRestore(context: PanelOperationContext, token: unknown): Promise<{
15
+ cancelled: boolean;
16
+ }>;
17
+ restore(context: PanelOperationContext, token: unknown): Promise<{
18
+ id: string;
19
+ packageDigest: string;
20
+ }>;
12
21
  assertAuthorized: (context: PanelOperationContext, expected?: number) => Promise<void>;
13
22
  discover(context: PanelOperationContext, input: unknown): Promise<import("./types.js").PanelDiscovery>;
14
23
  preview: (context: PanelOperationContext, input: unknown) => Promise<import("./types.js").PanelReview>;
15
24
  previewUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelReview>;
16
- install(context: PanelOperationContext, token: unknown, bind?: boolean): Promise<{
25
+ previewProjectSource: (context: PanelOperationContext, input: import("@cjhyy/code-shell-core").PanelAppSourceInput, expected?: {
26
+ id: string;
27
+ revision: string;
28
+ }) => Promise<import("./types.js").PanelProjectReview>;
29
+ previewProjectUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<import("./types.js").PanelProjectReview>;
30
+ install(context: PanelOperationContext, token: unknown, bind?: boolean, approval?: {
31
+ overwrite?: boolean;
32
+ expectedId?: string;
33
+ }): Promise<{
17
34
  id: string;
35
+ packageDigest: string | undefined;
18
36
  }>;
19
37
  binding(context: PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<import("./types.js").PanelSnapshot>;
20
38
  remove(context: PanelOperationContext, id: unknown, expected: unknown): Promise<{
@@ -83,12 +83,25 @@ export function createPanelManagementHttp(options) {
83
83
  const input = await body(request, ["reviewToken", "bind"]);
84
84
  result = await service.install(context, input.reviewToken, input.bind);
85
85
  }
86
+ else if (url.pathname === ROOT + "/restore" && method === "POST") {
87
+ result = await service.restore(context, (await body(request, ["reviewToken"])).reviewToken);
88
+ }
89
+ else if (url.pathname === ROOT + "/restore/review" && method === "DELETE") {
90
+ result = await service.cancelRestore(context, (await body(request, ["reviewToken"])).reviewToken);
91
+ }
86
92
  else {
87
- const match = /^\/api\/v1\/panels\/([^/]+)(?:\/(binding|update-preview))?$/.exec(url.pathname);
93
+ const match = /^\/api\/v1\/panels\/([^/]+)(?:\/(binding|update-preview|versions|restore-preview))?$/.exec(url.pathname);
88
94
  if (!match)
89
95
  throw new PanelManagementError(404, "not_found", "找不到这个面板操作。");
90
96
  const id = decodeURIComponent(match[1]);
91
- if (match[2] === "binding" && method === "PATCH") {
97
+ if (match[2] === "versions" && method === "POST") {
98
+ result = await service.packageHistory(context, id, (await body(request, ["expectedRevision"])).expectedRevision);
99
+ }
100
+ else if (match[2] === "restore-preview" && method === "POST") {
101
+ const input = await body(request, ["packageDigest", "expectedRevision"]);
102
+ result = await service.previewRestore(context, id, input.packageDigest, input.expectedRevision);
103
+ }
104
+ else if (match[2] === "binding" && method === "PATCH") {
92
105
  const input = await body(request, ["bound", "expectedRevision"]);
93
106
  result = await service.binding(context, id, input.bound, input.expectedRevision);
94
107
  }
@@ -1,6 +1,6 @@
1
- import { type PanelAppPreview } from "@cjhyy/code-shell-core";
1
+ import { type PanelAppPreview, type PanelAppSourceInput } from "@cjhyy/code-shell-core";
2
2
  import { getRefCommit } from "@cjhyy/code-shell-core/internal/skills";
3
- import type { PanelCompatibility, PanelDiscovery, PanelOperationContext, PanelReview, PanelSnapshot } from "./types.js";
3
+ import type { PanelCompatibility, PanelDiscovery, PanelOperationContext, PanelReview, PanelProjectReview, PanelSnapshot, PanelPackageHistory, PanelPackageRestoreReview } from "./types.js";
4
4
  export declare class PanelManagementError extends Error {
5
5
  readonly status: number;
6
6
  readonly code: string;
@@ -12,6 +12,10 @@ export interface PanelManagementOptions {
12
12
  cwd: string;
13
13
  /** Host-authorized main project for a worktree. Defaults to the exact cwd. */
14
14
  bindingCwd?: string;
15
+ /** Enable only when every runtime in this Host selects the project package. */
16
+ projectPackages?: boolean;
17
+ /** Explicit opt-in for a trusted native caller; never enabled on HTTP services. */
18
+ allowLocalSources?: boolean;
15
19
  /** Recheck a host-verified, frozen project/worktree association before use. */
16
20
  assertBinding?: () => void;
17
21
  withMutation?: <T>(write: () => Promise<T>) => Promise<T>;
@@ -22,13 +26,31 @@ export interface PanelManagementOptions {
22
26
  resolveCommit?: typeof getRefCommit;
23
27
  }
24
28
  export declare function createPanelManagement(options: PanelManagementOptions): {
25
- snapshot: () => Promise<PanelSnapshot>;
29
+ snapshot: (appId?: string) => Promise<PanelSnapshot>;
30
+ packageHistory(context: PanelOperationContext, id: unknown, expected: unknown): Promise<PanelPackageHistory>;
31
+ previewRestore(context: PanelOperationContext, id: unknown, digest: unknown, expected: unknown): Promise<PanelPackageRestoreReview>;
32
+ cancelRestore(context: PanelOperationContext, token: unknown): Promise<{
33
+ cancelled: boolean;
34
+ }>;
35
+ restore(context: PanelOperationContext, token: unknown): Promise<{
36
+ id: string;
37
+ packageDigest: string;
38
+ }>;
26
39
  assertAuthorized: (context: PanelOperationContext, expected?: number) => Promise<void>;
27
40
  discover(context: PanelOperationContext, input: unknown): Promise<PanelDiscovery>;
28
41
  preview: (context: PanelOperationContext, input: unknown) => Promise<PanelReview>;
29
42
  previewUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<PanelReview>;
30
- install(context: PanelOperationContext, token: unknown, bind?: boolean): Promise<{
43
+ previewProjectSource: (context: PanelOperationContext, input: PanelAppSourceInput, expected?: {
44
+ id: string;
45
+ revision: string;
46
+ }) => Promise<PanelProjectReview>;
47
+ previewProjectUpdate(context: PanelOperationContext, id: unknown, expected: unknown): Promise<PanelProjectReview>;
48
+ install(context: PanelOperationContext, token: unknown, bind?: boolean, approval?: {
49
+ overwrite?: boolean;
50
+ expectedId?: string;
51
+ }): Promise<{
31
52
  id: string;
53
+ packageDigest: string | undefined;
32
54
  }>;
33
55
  binding(context: PanelOperationContext, id: unknown, bound: unknown, expected: unknown): Promise<PanelSnapshot>;
34
56
  remove(context: PanelOperationContext, id: unknown, expected: unknown): Promise<{