@cjhyy/code-shell-server 0.9.22 → 0.9.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) 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/http.d.ts +11 -0
  51. package/dist/panels/resources/http.js +74 -0
  52. package/dist/panels/resources/references.d.ts +5 -0
  53. package/dist/panels/resources/references.js +26 -0
  54. package/dist/panels/runtime-services.js +16 -3
  55. package/dist/panels/runtime.d.ts +16 -4
  56. package/dist/panels/runtime.js +522 -64
  57. package/dist/panels/shared-tool-jobs.d.ts +40 -0
  58. package/dist/panels/shared-tool-jobs.js +37 -0
  59. package/dist/panels/storage-store.d.ts +21 -0
  60. package/dist/panels/storage-store.js +43 -0
  61. package/dist/panels/task-cookie-host.d.ts +26 -0
  62. package/dist/panels/task-cookie-host.js +145 -0
  63. package/dist/panels/task-cookies.d.ts +51 -0
  64. package/dist/panels/task-cookies.js +183 -0
  65. package/dist/panels/tool-executor.d.ts +21 -5
  66. package/dist/panels/tool-executor.js +171 -30
  67. package/dist/panels/tool-jobs-storage.d.ts +2 -0
  68. package/dist/panels/tool-jobs-storage.js +34 -0
  69. package/dist/panels/tool-jobs.d.ts +49 -3
  70. package/dist/panels/tool-jobs.js +270 -15
  71. package/dist/panels/types.d.ts +43 -0
  72. package/dist/project-runtime/backup.d.ts +26 -0
  73. package/dist/project-runtime/backup.js +500 -0
  74. package/dist/project-runtime/control-server.d.ts +4 -0
  75. package/dist/project-runtime/control-server.js +19 -1
  76. package/dist/project-runtime/docker-provider.d.ts +4 -0
  77. package/dist/project-runtime/docker-provider.js +16 -1
  78. package/dist/project-runtime/managed-entry.js +4 -0
  79. package/dist/project-runtime/registry.d.ts +2 -0
  80. package/dist/project-runtime/registry.js +15 -0
  81. package/dist/project-runtime/seccomp.d.ts +8 -0
  82. package/dist/project-runtime/seccomp.js +72 -0
  83. package/dist/project-runtime/types.d.ts +2 -0
  84. package/dist/remote-relay/byte-stream.d.ts +9 -0
  85. package/dist/remote-relay/byte-stream.js +71 -0
  86. package/dist/remote-relay/connector.d.ts +27 -0
  87. package/dist/remote-relay/connector.js +197 -0
  88. package/dist/remote-relay/local-stream.d.ts +10 -0
  89. package/dist/remote-relay/local-stream.js +120 -0
  90. package/dist/remote-relay/protocol.d.ts +39 -0
  91. package/dist/remote-relay/protocol.js +64 -0
  92. package/dist/serve/automation-worker.d.ts +16 -0
  93. package/dist/serve/automation-worker.js +126 -0
  94. package/dist/serve/cli.d.ts +3 -0
  95. package/dist/serve/cli.js +17 -0
  96. package/dist/serve/headless-server.d.ts +3 -0
  97. package/dist/serve/headless-server.js +140 -10
  98. package/dist/serve/settings-recovery-cli.d.ts +29 -0
  99. package/dist/serve/settings-recovery-cli.js +128 -0
  100. package/dist/session-http-responses.d.ts +7 -0
  101. package/dist/session-http-responses.js +40 -0
  102. package/dist/worker-bridge-core.d.ts +5 -0
  103. package/dist/worker-bridge-core.js +23 -0
  104. package/package.json +15 -6
@@ -0,0 +1,126 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { HubAutomationUncertainError, HubAutomationCancelledError, } from "../panels/hub-automations.js";
3
+ /** Borrows the project's live Worker so persisted Session state has one writer. */
4
+ export function createHubAutomationWorker(options) {
5
+ const active = new Map();
6
+ const rpc = (method, params, id = `automation-control-${randomUUID()}`) => options.bridge.request(method, params, {
7
+ id,
8
+ consume: true,
9
+ settleOnExit: true,
10
+ failFast: true,
11
+ timeoutMs: 5000,
12
+ meta: { origin: "host", producer: "hub-automation" },
13
+ });
14
+ /** Called before the interactive approval lease router. */
15
+ function handleApproval(params) {
16
+ const entry = active.get(params.sessionId);
17
+ if (!entry)
18
+ return false;
19
+ if (typeof params.requestId !== "string" ||
20
+ !params.request ||
21
+ typeof params.request.toolName !== "string")
22
+ return true;
23
+ if (entry.approvals.has(params.requestId))
24
+ return true;
25
+ entry.approvals.add(params.requestId);
26
+ void (async () => {
27
+ const request = params.request;
28
+ const decision = request.toolName.startsWith("__")
29
+ ? {
30
+ approved: false,
31
+ failure: "unavailable",
32
+ reason: "Unattended runs cannot use a page-owned capability",
33
+ }
34
+ : await entry.request.approvalBackend.requestApproval(request);
35
+ if (active.get(params.sessionId) !== entry || entry.request.signal?.aborted)
36
+ return;
37
+ const outcome = await rpc("agent/approve", {
38
+ sessionId: params.sessionId,
39
+ requestId: params.requestId,
40
+ connectionId: params.connectionId,
41
+ generation: params.generation,
42
+ decision,
43
+ });
44
+ if (outcome.status !== "result" && active.get(params.sessionId) === entry) {
45
+ await rpc("agent/cancel", { sessionId: params.sessionId });
46
+ }
47
+ })().catch(async () => {
48
+ if (active.get(params.sessionId) === entry)
49
+ await rpc("agent/cancel", { sessionId: params.sessionId });
50
+ });
51
+ return true;
52
+ }
53
+ async function execute(request) {
54
+ request.signal?.throwIfAborted();
55
+ const sessionId = request.job.resumeSessionId;
56
+ if (!sessionId || request.job.cwd !== options.cwd || !request.job.lastRunId)
57
+ throw Error("Automation requires a persisted run identity and project Session");
58
+ if (active.has(sessionId))
59
+ throw Error("The Session already has an active automation");
60
+ const requestId = `automation-run-${request.job.lastRunId}`;
61
+ const release = options.reserve(request.job, requestId);
62
+ const entry = { request, requestId, approvals: new Set() };
63
+ active.set(sessionId, entry);
64
+ let cancel;
65
+ try {
66
+ const model = options.model();
67
+ if (!model)
68
+ throw Error("Configure a default text model before running automations");
69
+ request.signal?.throwIfAborted();
70
+ const result = options.bridge.request("agent/run", {
71
+ sessionId,
72
+ cwd: options.cwd,
73
+ task: request.prompt,
74
+ model,
75
+ requireExisting: true,
76
+ clientMessageId: requestId,
77
+ displayText: request.prompt,
78
+ disableGoal: true,
79
+ permissionMode: request.permissionMode,
80
+ sandboxMode: request.sandboxMode,
81
+ allowBackgroundShells: false,
82
+ }, {
83
+ id: requestId,
84
+ timeoutMs: options.admissionTimeoutMs ?? 60000,
85
+ consume: true,
86
+ settleOnExit: true,
87
+ failFast: true,
88
+ waitForRunCompletion: true,
89
+ ensureWorker: true,
90
+ ensureWorkerCwd: options.cwd,
91
+ meta: { origin: "host", producer: "hub-automation" },
92
+ });
93
+ cancel = () => {
94
+ void rpc("agent/cancel", { sessionId });
95
+ };
96
+ request.signal?.addEventListener("abort", cancel, { once: true });
97
+ if (request.signal?.aborted)
98
+ cancel();
99
+ const outcome = await result;
100
+ if (outcome.status === "timeout" || outcome.status === "sendFailed") {
101
+ // Admission may have reached the child despite a lost ACK/write error.
102
+ // Never release the Session based only on a local transport timeout.
103
+ await options.bridge.stopAndWait();
104
+ throw new HubAutomationUncertainError("Worker admission was not confirmed; inspect results before resuming");
105
+ }
106
+ if (outcome.status === "workerExit")
107
+ throw new HubAutomationUncertainError("Worker exited before recording the automation outcome");
108
+ if (outcome.status === "error")
109
+ throw Error(outcome.error.message ?? "Automation Worker rejected the run");
110
+ const value = outcome.result;
111
+ if (request.signal?.aborted)
112
+ return;
113
+ if (value?.reason === "aborted_streaming" || value?.reason === "aborted_tools")
114
+ throw new HubAutomationCancelledError("Automation was cancelled");
115
+ if (value?.reason !== "completed")
116
+ throw Error(`Automation ended without completion (${value?.reason ?? "unknown"})`);
117
+ }
118
+ finally {
119
+ if (cancel)
120
+ request.signal?.removeEventListener("abort", cancel);
121
+ active.delete(sessionId);
122
+ release();
123
+ }
124
+ }
125
+ return { execute, handleApproval, ownsSession: (sessionId) => active.has(sessionId) };
126
+ }
@@ -1,3 +1,4 @@
1
+ import type { RemoteLinkConfiguration } from "@cjhyy/code-shell-core";
1
2
  interface CliArgs {
2
3
  cwd: string;
3
4
  host: string;
@@ -6,8 +7,10 @@ interface CliArgs {
6
7
  authMode: "hub" | "passcode";
7
8
  runtime: "local" | "docker";
8
9
  runtimeImage?: string;
10
+ runtimeSeccompProfile?: string;
9
11
  debugLogs?: boolean;
10
12
  publicOrigin?: string;
13
+ remoteLink?: RemoteLinkConfiguration;
11
14
  dataDir: string;
12
15
  staticRootDir?: string;
13
16
  }
package/dist/serve/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { remoteLinkFromEnvironment } from "../links/remote-configuration.js";
1
2
  // packages/server/src/serve/cli.ts
2
3
  //
3
4
  // `code-shell-serve` boots a single-workspace Hub under Node.js.
@@ -20,6 +21,7 @@ Options:
20
21
  --auth <mode> hub (default) or legacy passcode
21
22
  --runtime <mode> local (default) or Docker project sandboxes
22
23
  --runtime-image <tag> Prebuilt project image (default: codeshell-project-runtime:local)
24
+ --runtime-seccomp-profile <path> Administrator-owned Docker seccomp JSON
23
25
  --public-url <url> External HTTPS origin for reverse-proxy deployments
24
26
  --passcode <code> Set or rotate the legacy passcode (selects passcode mode)
25
27
  --data-dir <path> Persistent server data (auth, uploads and worker sessions)
@@ -34,6 +36,7 @@ const VALUE_FLAGS = new Set([
34
36
  "--auth",
35
37
  "--runtime",
36
38
  "--runtime-image",
39
+ "--runtime-seccomp-profile",
37
40
  "--public-url",
38
41
  "--data-dir",
39
42
  "--static-root",
@@ -73,6 +76,10 @@ export function parseServeArgs(argv, env = process.env) {
73
76
  throw new Error("Docker projects require --auth hub");
74
77
  if (args["runtime-image"] && runtime !== "docker")
75
78
  throw new Error("--runtime-image requires --runtime docker");
79
+ if (args["runtime-seccomp-profile"] !== undefined && runtime !== "docker")
80
+ throw new Error("--runtime-seccomp-profile requires --runtime docker");
81
+ if (args["runtime-seccomp-profile"] === "")
82
+ throw new Error("Empty runtime seccomp profile");
76
83
  let publicOrigin;
77
84
  const publicUrl = args["public-url"] ?? env.CODE_SHELL_SERVE_PUBLIC_URL;
78
85
  if (publicUrl) {
@@ -88,10 +95,17 @@ export function parseServeArgs(argv, env = process.env) {
88
95
  }
89
96
  publicOrigin = url.origin;
90
97
  }
98
+ const remoteLink = remoteLinkFromEnvironment(env, publicOrigin);
99
+ if (remoteLink && authMode !== "hub")
100
+ throw new Error("Remote Link requires --auth hub");
91
101
  return {
92
102
  authMode,
103
+ ...(remoteLink ? { remoteLink } : {}),
93
104
  runtime,
94
105
  ...(args["runtime-image"] ? { runtimeImage: args["runtime-image"] } : {}),
106
+ ...(args["runtime-seccomp-profile"]
107
+ ? { runtimeSeccompProfile: resolve(args["runtime-seccomp-profile"]) }
108
+ : {}),
95
109
  ...(args["debug-logs"] ? { debugLogs: true } : {}),
96
110
  ...(publicOrigin ? { publicOrigin } : {}),
97
111
  cwd: resolve(args.cwd ?? process.cwd()),
@@ -143,8 +157,10 @@ export async function runServeCli(argv = process.argv.slice(2)) {
143
157
  port: parsed.port,
144
158
  dataDir: parsed.dataDir,
145
159
  publicOrigin: parsed.publicOrigin,
160
+ remoteLink: parsed.remoteLink,
146
161
  staticRootDir,
147
162
  runtimeImage: parsed.runtimeImage,
163
+ runtimeSeccompProfile: parsed.runtimeSeccompProfile,
148
164
  })
149
165
  : await startHeadlessServer({
150
166
  host: parsed.host,
@@ -152,6 +168,7 @@ export async function runServeCli(argv = process.argv.slice(2)) {
152
168
  cwd: parsed.cwd,
153
169
  dataDir: parsed.dataDir,
154
170
  authMode: parsed.authMode,
171
+ remoteLink: parsed.remoteLink,
155
172
  debugLogs: parsed.debugLogs,
156
173
  ...(parsed.publicOrigin ? { publicOrigin: parsed.publicOrigin } : {}),
157
174
  workerEntryPath: resolveWorkerEntry(),
@@ -1,3 +1,4 @@
1
+ import type { RemoteLinkConfiguration } from "@cjhyy/code-shell-core";
1
2
  import { AccessPasscode } from "../mobile-remote/access-passcode.js";
2
3
  import { WorkerBridgeCore, type WorkerBridgeLog } from "../worker-bridge-core.js";
3
4
  export interface HeadlessServeOptions {
@@ -36,6 +37,8 @@ export interface HeadlessServeOptions {
36
37
  debugLogs?: boolean;
37
38
  /** Exact external origin; required for HTTPS reverse proxies. Never trust forwarded headers. */
38
39
  publicOrigin?: string;
40
+ /** Trusted deployment configuration; not a browser-supplied provider URL. */
41
+ remoteLink?: RemoteLinkConfiguration;
39
42
  /** Trusted project gateway prefix; generated by the controller, never a request parameter. */
40
43
  publicPathPrefix?: string;
41
44
  /** Session revocation/expiry sweep interval; defaults to 5 seconds. */
@@ -1,3 +1,7 @@
1
+ import { createHubPanelAutomationHost } from "../panels/hub-automations.js";
2
+ import { createHubAutomationWorker } from "./automation-worker.js";
3
+ import { remoteLinkHostConfiguration } from "../links/remote-configuration.js";
4
+ import { describeEnvironment, environmentIdentity } from "../environment-identity.js";
1
5
  // packages/server/src/serve/headless-server.ts
2
6
  //
3
7
  // Single-workspace Node host. The CLI uses Hub administrator sessions; the
@@ -34,8 +38,12 @@ import { createPanelHttp } from "../panels/http.js";
34
38
  import { createHubPanelBinding } from "../panels/hub-binding.js";
35
39
  import { createHubSessions, hubSessionPreview, readHubSessionState, readHubTranscript, } from "../hub/session-management.js";
36
40
  import { createHubFiles } from "../hub/files.js";
41
+ import { createSessionHttpResponses } from "../session-http-responses.js";
37
42
  import { WorkerBridgeCore, previewLine } from "../worker-bridge-core.js";
38
43
  export async function startHeadlessServer(opts) {
44
+ const remoteLink = remoteLinkHostConfiguration(opts.remoteLink, opts.publicOrigin);
45
+ if (remoteLink && opts.authMode !== "hub")
46
+ throw new Error("Remote Link requires Hub authentication.");
39
47
  const host = opts.host ?? "127.0.0.1";
40
48
  const log = (event, data) => {
41
49
  if (!opts.log)
@@ -45,6 +53,9 @@ export async function startHeadlessServer(opts) {
45
53
  const { raw: _raw, text: _text, ...metadata } = data ?? {};
46
54
  opts.log(event, opts.debugLogs ? data : metadata);
47
55
  };
56
+ const environment = opts.authMode === "hub"
57
+ ? describeEnvironment(await environmentIdentity(opts.dataDir), "hub")
58
+ : undefined;
48
59
  const workspaceCwd = resolve(opts.cwd);
49
60
  const panelBinding = opts.authMode === "hub" ? createHubPanelBinding(workspaceCwd) : undefined;
50
61
  const workerDataRoot = resolve(opts.workerDataRoot ??
@@ -72,6 +83,10 @@ export async function startHeadlessServer(opts) {
72
83
  generatedPasscode = randomBytes(6).toString("base64url");
73
84
  passcode.set(generatedPasscode);
74
85
  }
86
+ let closing = false;
87
+ const responses = createSessionHttpResponses();
88
+ let panelAutomations;
89
+ let automationWorker;
75
90
  const tabs = new Set();
76
91
  // Auth revocation callbacks are registered before configuration modules are ready.
77
92
  // eslint-disable-next-line prefer-const
@@ -88,6 +103,7 @@ export async function startHeadlessServer(opts) {
88
103
  dataDir: opts.dataDir,
89
104
  publicOrigin: opts.publicOrigin,
90
105
  onRevoke: (sessionId) => {
106
+ responses.cancelOwner(sessionId);
91
107
  mcp?.cancelOwner(sessionId);
92
108
  configuration?.cancelOwner(sessionId);
93
109
  links?.cancelOwner(sessionId);
@@ -112,7 +128,7 @@ export async function startHeadlessServer(opts) {
112
128
  let configurationChanging = false;
113
129
  let configurationReloadFailed = false;
114
130
  const runUploads = new Map();
115
- const isRunning = (sessionId) => [...runningSessions.values()].includes(sessionId);
131
+ const isRunning = (sessionId) => [...runningSessions.values()].includes(sessionId) || !!automationWorker?.ownsSession(sessionId);
116
132
  let nextTabId = 1;
117
133
  let nextWorkerRequestId = 1;
118
134
  const outbound = new HubOutboundTransport({
@@ -254,23 +270,35 @@ export async function startHeadlessServer(opts) {
254
270
  if (approval &&
255
271
  typeof approval.requestId === "string" &&
256
272
  typeof approval.sessionId === "string") {
273
+ if (automationWorker?.handleApproval(approval))
274
+ return;
257
275
  const request = approval.request;
258
276
  if (request?.toolName === "__panel_action__") {
259
277
  const ownerId = runOwners.get(approval.sessionId);
260
278
  const result = ownerId && panels
261
279
  ? panels.panelAction(ownerId, approval.sessionId, request.args ?? {})
262
280
  : Promise.resolve({ ok: false, detail: "当前对话没有可用的面板连接。" });
263
- void result.catch(() => ({ ok: false, detail: "面板操作未完成。" })).then((answer) => {
281
+ void result
282
+ .catch(() => ({ ok: false, detail: "面板操作未完成。" }))
283
+ .then((answer) => {
264
284
  if (!isRunning(approval.sessionId) || runOwners.get(approval.sessionId) !== ownerId)
265
285
  return;
266
286
  return bridge.request("agent/approve", {
267
- sessionId: approval.sessionId, requestId: approval.requestId,
268
- connectionId: approval.connectionId, generation: approval.generation,
287
+ sessionId: approval.sessionId,
288
+ requestId: approval.requestId,
289
+ connectionId: approval.connectionId,
290
+ generation: approval.generation,
269
291
  decision: { approved: true, answer: JSON.stringify(answer) },
270
- }, { id: `panel-reply-${randomUUID()}`, consume: true, settleOnExit: true, failFast: true, timeoutMs: 5000,
292
+ }, {
293
+ id: `panel-reply-${randomUUID()}`,
294
+ consume: true,
295
+ settleOnExit: true,
296
+ failFast: true,
297
+ timeoutMs: 5000,
271
298
  meta: { origin: "host", producer: "hub-panel-action" },
272
299
  });
273
- }).catch(() => { });
300
+ })
301
+ .catch(() => { });
274
302
  return;
275
303
  }
276
304
  try {
@@ -346,6 +374,7 @@ export async function startHeadlessServer(opts) {
346
374
  fallbackCwd: () => opts.cwd,
347
375
  buildEnv: () => ({
348
376
  ...process.env,
377
+ CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
349
378
  CODE_SHELL_DATA_ROOT: workerDataRoot,
350
379
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
351
380
  ...(opts.workerCapabilityModules
@@ -377,6 +406,7 @@ export async function startHeadlessServer(opts) {
377
406
  preparingRuns > 0 ||
378
407
  runningSessions.size > 0 ||
379
408
  (panels?.activeTaskCount() ?? 0) > 0 ||
409
+ (panelAutomations?.activeCount() ?? 0) > 0 ||
380
410
  pendingWorkerResponses.size > 0) {
381
411
  throw new HubConfigurationError(409, "请等待当前任务完成后再保存配置。");
382
412
  }
@@ -420,6 +450,7 @@ export async function startHeadlessServer(opts) {
420
450
  links = hubAuth
421
451
  ? createLinkHttp({
422
452
  ...configurationOptions,
453
+ remoteLink: () => remoteLink,
423
454
  onChanged: () => notify("serve/configurationChanged", {}),
424
455
  })
425
456
  : undefined;
@@ -429,14 +460,25 @@ export async function startHeadlessServer(opts) {
429
460
  ...panelBinding,
430
461
  dataDir: opts.dataDir,
431
462
  host: "hub",
463
+ automations: {
464
+ conditionalMutations: true,
465
+ call: (scope, method, params) => {
466
+ if (!panelAutomations || closing)
467
+ throw Error("Cloud automations are unavailable");
468
+ return panelAutomations.host.call(scope, method, params);
469
+ },
470
+ },
432
471
  publicPathPrefix: opts.publicPathPrefix,
433
472
  agentTaskOptions: {
434
473
  workerEntryPath: opts.workerEntryPath,
435
474
  buildEnv: () => ({
436
475
  ...process.env,
476
+ CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
437
477
  CODE_SHELL_DATA_ROOT: workerDataRoot,
438
478
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
439
- ...(opts.workerCapabilityModules ? { CODE_SHELL_CAPABILITY_MODULES: opts.workerCapabilityModules } : {}),
479
+ ...(opts.workerCapabilityModules
480
+ ? { CODE_SHELL_CAPABILITY_MODULES: opts.workerCapabilityModules }
481
+ : {}),
440
482
  }),
441
483
  },
442
484
  onChanged: () => notify("serve/configurationChanged", {}),
@@ -535,6 +577,15 @@ export async function startHeadlessServer(opts) {
535
577
  hubJson(res, 403, { error: "origin rejected" });
536
578
  return;
537
579
  }
580
+ if (closing) {
581
+ hubJson(res, 503, { error: "server is closing" });
582
+ return;
583
+ }
584
+ responses.track(session.id, res);
585
+ if (pathname === "/api/v1/environment" && req.method === "GET") {
586
+ hubJson(res, 200, environment);
587
+ return;
588
+ }
538
589
  if (await configuration.handle(req, res))
539
590
  return;
540
591
  if (await links.handle(req, res))
@@ -616,6 +667,10 @@ export async function startHeadlessServer(opts) {
616
667
  ws.close(1009, "payload too large");
617
668
  return;
618
669
  }
670
+ if (closing) {
671
+ ws.close(1012, "server stopping");
672
+ return;
673
+ }
619
674
  const line = String(data);
620
675
  // Validate framing before touching the worker: a malformed frame from
621
676
  // one tab must never kill the shared pipe.
@@ -798,7 +853,8 @@ export async function startHeadlessServer(opts) {
798
853
  const runSession = workerMessage.params?.sessionId;
799
854
  const ownerId = tabAuth.get(ws)?.sessionId;
800
855
  const activeOwner = typeof runSession === "string" ? runOwners.get(runSession) : undefined;
801
- if (activeOwner && activeOwner !== ownerId) {
856
+ if ((activeOwner && activeOwner !== ownerId) ||
857
+ (typeof runSession === "string" && automationWorker?.ownsSession(runSession))) {
802
858
  sendToTab(ws, hostQueryError(workerMessage.id, -32009, "此对话正在另一台设备运行,请等待完成后再发送。"), "run-owner");
803
859
  return;
804
860
  }
@@ -900,6 +956,69 @@ export async function startHeadlessServer(opts) {
900
956
  void upgrade(req, socket, head).catch(() => socket.destroy());
901
957
  });
902
958
  try {
959
+ if (hubAuth) {
960
+ automationWorker = createHubAutomationWorker({
961
+ bridge,
962
+ cwd: workspaceCwd,
963
+ model: () => new SettingsManager(workspaceCwd, "full").get().defaults.text,
964
+ reserve: (job, requestId) => {
965
+ if (closing || configurationChanging || configurationReloadFailed || preparingRuns > 0)
966
+ throw Error("Project configuration or run preparation is busy");
967
+ const sessionId = job.resumeSessionId;
968
+ if (isRunning(sessionId) || runningSessions.size >= 64)
969
+ throw Error("The automation Session is busy; this occurrence was not dispatched");
970
+ if (resolve(readHubSessionState(sessionRootDir, sessionId).cwd) !== workspaceCwd)
971
+ throw Error("Automation Session changed workspace");
972
+ const baseline = readHubTranscript(sessionRootDir, sessionId).length;
973
+ runningSessions.set(requestId, sessionId);
974
+ runOwners.set(sessionId, requestId);
975
+ runReplay.begin(sessionId, requestId, baseline);
976
+ runInputs.set(requestId, {
977
+ sessionId,
978
+ echoed: false,
979
+ event: {
980
+ type: "session_user_message",
981
+ text: job.prompt,
982
+ clientMessageId: requestId,
983
+ attachments: [],
984
+ },
985
+ });
986
+ notify("serve/sessionStatus", { sessionId, running: true });
987
+ return () => {
988
+ runningSessions.delete(requestId);
989
+ runInputs.delete(requestId);
990
+ if (runOwners.get(sessionId) === requestId)
991
+ runOwners.delete(sessionId);
992
+ runReplay.finish(sessionId, requestId);
993
+ notify("serve/sessionStatus", { sessionId, running: isRunning(sessionId) });
994
+ };
995
+ },
996
+ });
997
+ panelAutomations = createHubPanelAutomationHost({
998
+ cwd: workspaceCwd,
999
+ bindingCwd: panelBinding?.bindingCwd,
1000
+ dataDir: opts.dataDir,
1001
+ sessionRootDir,
1002
+ assertExecutable: async (job) => {
1003
+ panelBinding?.assertBinding();
1004
+ const panel = (await panels.service.snapshot()).panels.find((app) => app.id === job.panelSource?.appId);
1005
+ panelBinding?.assertBinding();
1006
+ if (!panel?.enabled ||
1007
+ panel.revision !== job.panelSource?.revision ||
1008
+ !["automations.manage", "context.workspace", "context.session"].every((permission) => panel.permissions.includes(permission)))
1009
+ throw Error("The Panel package or automation permission is no longer authorized");
1010
+ },
1011
+ execute: (request) => automationWorker.execute(request),
1012
+ onJobEvent: (event) => notify("serve/automationStatus", {
1013
+ type: event.type,
1014
+ jobId: event.job.id,
1015
+ sessionId: event.job.resumeSessionId,
1016
+ appId: event.job.panelSource?.appId,
1017
+ ...("reason" in event ? { reason: event.reason } : {}),
1018
+ ...("error" in event ? { error: event.error } : {}),
1019
+ }),
1020
+ });
1021
+ }
903
1022
  await new Promise((resolve, reject) => {
904
1023
  server.once("error", reject);
905
1024
  server.listen(opts.port ?? 8790, host, () => {
@@ -909,6 +1028,9 @@ export async function startHeadlessServer(opts) {
909
1028
  });
910
1029
  }
911
1030
  catch (error) {
1031
+ closing = true;
1032
+ responses.clear();
1033
+ await panelAutomations?.close();
912
1034
  clearInterval(pendingResponseReaper);
913
1035
  if (authReaper)
914
1036
  clearInterval(authReaper);
@@ -919,7 +1041,7 @@ export async function startHeadlessServer(opts) {
919
1041
  links?.close();
920
1042
  await panels?.close();
921
1043
  files?.close();
922
- bridge.kill();
1044
+ await bridge.stopAndWait();
923
1045
  wss.close();
924
1046
  server.close();
925
1047
  throw error;
@@ -939,6 +1061,9 @@ export async function startHeadlessServer(opts) {
939
1061
  tabCount: () => tabs.size,
940
1062
  pendingResponseCount: () => pendingWorkerResponses.size,
941
1063
  close: async () => {
1064
+ closing = true;
1065
+ responses.clear();
1066
+ await panelAutomations?.close();
942
1067
  clearInterval(pendingResponseReaper);
943
1068
  if (authReaper)
944
1069
  clearInterval(authReaper);
@@ -958,7 +1083,7 @@ export async function startHeadlessServer(opts) {
958
1083
  }
959
1084
  }
960
1085
  tabs.clear();
961
- bridge.kill();
1086
+ await bridge.stopAndWait();
962
1087
  await new Promise((resolve) => {
963
1088
  wss.close(() => {
964
1089
  server.close(() => resolve());
@@ -1077,6 +1202,11 @@ function authorizeServeRequest(message, sessionManager, workspaceCwd, isRunning
1077
1202
  if (!message.method || !SERVE_ALLOWED_WORKER_METHODS.has(message.method)) {
1078
1203
  return hostQueryError(message.id ?? null, -32601, "Method is not available in Web serve mode");
1079
1204
  }
1205
+ if (message.method === "agent/run" &&
1206
+ (Object.hasOwn(message.params ?? {}, "sandboxMode") ||
1207
+ Object.hasOwn(message.params ?? {}, "allowBackgroundShells"))) {
1208
+ return hostQueryError(message.id ?? null, -32602, "Execution policy is selected by the host");
1209
+ }
1080
1210
  const rawSessionId = message.params?.sessionId;
1081
1211
  const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : null;
1082
1212
  if (message.method === "agent/run" && sessionId === null)
@@ -0,0 +1,29 @@
1
+ import type { RecoveryScope } from "@cjhyy/code-shell-core/internal";
2
+ type RecoveryArguments = {
3
+ action: "help";
4
+ } | {
5
+ action: "inspect";
6
+ cwd: string;
7
+ scope: RecoveryScope;
8
+ candidatePath?: string;
9
+ } | {
10
+ action: "repair";
11
+ cwd: string;
12
+ scope: RecoveryScope;
13
+ candidatePath: string;
14
+ expectedRevision: string;
15
+ candidateSha256: string;
16
+ } | {
17
+ action: "restore";
18
+ cwd: string;
19
+ scope: RecoveryScope;
20
+ backupId: string;
21
+ expectedRevision: string;
22
+ };
23
+ export declare function parseSettingsRecoveryArgs(argv: string[], cwd: () => string): RecoveryArguments;
24
+ export declare function runSettingsRecoveryCli(argv?: string[], io?: {
25
+ cwd?: () => string;
26
+ write?: (text: string) => void;
27
+ error?: (text: string) => void;
28
+ }): Promise<number>;
29
+ export {};