@cjhyy/code-shell-server 0.9.41 → 0.9.42

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.
@@ -261,6 +261,9 @@ export function createPanelAgentTaskHost(options = {}) {
261
261
  buildEnv: options.buildEnv ??
262
262
  (() => ({
263
263
  ...process.env,
264
+ // These switch on Desktop-main-only IPC clients in the worker.
265
+ CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
266
+ CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
264
267
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
265
268
  CODE_SHELL_CAPABILITY_MODULES: `${import.meta.resolve("@cjhyy/code-shell-capability-coding")}#createCodingModule`,
266
269
  })),
@@ -1,4 +1,4 @@
1
- export declare const panelRuntimeApiVersion = 14;
1
+ export declare const panelRuntimeApiVersion: 14;
2
2
  export declare const panelProcessMethods: string[];
3
3
  export declare const panelResourceMethods: string[];
4
4
  export declare const panelToolJobMethods: string[];
@@ -1,5 +1,6 @@
1
+ import { CONTRACTS } from "@cjhyy/code-shell-core";
1
2
  import { processLimits } from "./process-state.js";
2
- export const panelRuntimeApiVersion = 14;
3
+ export const panelRuntimeApiVersion = CONTRACTS.panelApi;
3
4
  export const panelProcessMethods = [
4
5
  "process.find",
5
6
  "process.info",
@@ -1,6 +1,6 @@
1
1
  import { type CloudAccountBinding } from "./account-binding.js";
2
2
  import type { CloudAccountAuthority, CloudManagementSession, ProjectControlAuthority } from "./control-authority.js";
3
- export declare const CLOUD_MANAGEMENT_AUTHORITY_VERSION = 2;
3
+ export declare const CLOUD_MANAGEMENT_AUTHORITY_VERSION: 2;
4
4
  export declare const CLOUD_MANAGEMENT_COOKIE = "cs_cloud_management";
5
5
  export declare function cloudManagementSessionView(session: CloudManagementSession): {
6
6
  id: string;
@@ -1,7 +1,8 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { CONTRACTS } from "@cjhyy/code-shell-core";
2
3
  import { isCloudAccountOrigin } from "./account-binding.js";
3
4
  import { ProjectRegistryError } from "./registry.js";
4
- export const CLOUD_MANAGEMENT_AUTHORITY_VERSION = 2;
5
+ export const CLOUD_MANAGEMENT_AUTHORITY_VERSION = CONTRACTS.cloudManagementAuthority;
5
6
  export const CLOUD_MANAGEMENT_COOKIE = "cs_cloud_management";
6
7
  const TOKEN = /^[A-Za-z0-9_-]{43}$/;
7
8
  const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
@@ -1,6 +1,6 @@
1
1
  import type { CloudAccountAuthority, ProjectControlAuthority } from "./control-authority.js";
2
2
  /** Browser bootstrap is separate from the native, single-operator v2 API. */
3
- export declare const CLOUD_BROWSER_AUTHORITY_VERSION = 1;
3
+ export declare const CLOUD_BROWSER_AUTHORITY_VERSION: 1;
4
4
  export declare const CLOUD_BROWSER_COOKIE = "cs_cloud_browser";
5
5
  /** Every principal is independently scoped; a login never claims legacy projects. */
6
6
  export declare function createCloudBrowserAuth(options: {
@@ -1,9 +1,10 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { CONTRACTS } from "@cjhyy/code-shell-core";
2
3
  import { isCloudAccountOrigin } from "./account-binding.js";
3
4
  import { cloudManagementSessionView } from "./cloud-account-auth.js";
4
5
  import { ProjectRegistryError } from "./registry.js";
5
6
  /** Browser bootstrap is separate from the native, single-operator v2 API. */
6
- export const CLOUD_BROWSER_AUTHORITY_VERSION = 1;
7
+ export const CLOUD_BROWSER_AUTHORITY_VERSION = CONTRACTS.cloudBrowserAuthority;
7
8
  export const CLOUD_BROWSER_COOKIE = "cs_cloud_browser";
8
9
  const TOKEN = /^[A-Za-z0-9_-]{43}$/;
9
10
  const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
@@ -21,7 +21,7 @@ import { readFileSync, existsSync } from "node:fs";
21
21
  import { basename, dirname, extname, join, resolve } from "node:path";
22
22
  import { randomBytes, randomUUID } from "node:crypto";
23
23
  import { WebSocketServer } from "ws";
24
- import { SessionManager, SettingsManager } from "@cjhyy/code-shell-core";
24
+ import { CONTRACTS, SessionManager, SettingsManager } from "@cjhyy/code-shell-core";
25
25
  import { SessionOutputCoverageStore, readOutputJournal, readOutputJournalLegacyBase, } from "@cjhyy/code-shell-core/internal";
26
26
  import { AccessPasscode } from "../mobile-remote/access-passcode.js";
27
27
  import { resolveSafe } from "../mobile-remote/mobile-static.js";
@@ -42,6 +42,7 @@ import { createHubSessions, hubSessionPreview, readHubSessionState, readHubTrans
42
42
  import { createHubFiles } from "../hub/files.js";
43
43
  import { createSessionHttpResponses } from "../session-http-responses.js";
44
44
  import { WorkerBridgeCore, previewLine } from "../worker-bridge-core.js";
45
+ import { isHostAuthorityRequest, unsupportedHostRequestResponse, unsupportedLoopbackAnswer, unsupportedLoopbackCapability, } from "./host-requests.js";
45
46
  export async function startHeadlessServer(opts) {
46
47
  const remoteLink = remoteLinkHostConfiguration(opts.remoteLink, opts.publicOrigin);
47
48
  if (remoteLink && opts.authMode !== "hub")
@@ -226,6 +227,48 @@ export async function startHeadlessServer(opts) {
226
227
  }
227
228
  }, pendingResponseReaperMs);
228
229
  pendingResponseReaper.unref?.();
230
+ // Answer loopbacks only Desktop main can serve, instead of leasing them to a
231
+ // Web tab that has no way to fulfil them (ch.16 §3.2 step 1.1). Hub mode
232
+ // serves Panel actions itself; passcode mode has no Panel runtime.
233
+ const serveCapabilities = { panels: Boolean(hubAuth) };
234
+ const replyUnsupportedLoopback = (params) => {
235
+ const approval = params;
236
+ if (!approval ||
237
+ typeof approval.requestId !== "string" ||
238
+ typeof approval.sessionId !== "string")
239
+ return false;
240
+ const request = approval.request;
241
+ const capability = unsupportedLoopbackCapability(request?.toolName, serveCapabilities);
242
+ if (!capability)
243
+ return false;
244
+ // Core's child-browser activate/release frames are one-way notifications
245
+ // (protocol/server.ts childHost.activate / childHost.release): their
246
+ // requestId is never registered, so there is nothing to answer.
247
+ const childHost = approval.childHost;
248
+ if (childHost?.activate === true || childHost?.release === true)
249
+ return true;
250
+ log("worker.loopback_unsupported", { capability });
251
+ void bridge
252
+ .request("agent/approve", {
253
+ sessionId: approval.sessionId,
254
+ requestId: approval.requestId,
255
+ connectionId: approval.connectionId,
256
+ generation: approval.generation,
257
+ decision: { approved: true, answer: unsupportedLoopbackAnswer(capability) },
258
+ }, {
259
+ id: `unsupported-reply-${randomUUID()}`,
260
+ consume: true,
261
+ settleOnExit: true,
262
+ failFast: true,
263
+ timeoutMs: 5000,
264
+ meta: { origin: "host", producer: "serve-unsupported-loopback" },
265
+ })
266
+ .catch((e) => log("worker.loopback_unsupported_failed", {
267
+ capability,
268
+ reason: e instanceof Error ? e.message : "error",
269
+ }));
270
+ return true;
271
+ };
229
272
  // Notifications describe shared agent state and are broadcast. Correlated
230
273
  // JSON-RPC responses must return only to the tab that issued the request;
231
274
  // browser tabs all start their local counters at `web-1`, so broadcasting a
@@ -240,6 +283,15 @@ export async function startHeadlessServer(opts) {
240
283
  return;
241
284
  }
242
285
  if (!message || message.id === undefined || message.method !== undefined) {
286
+ if (message && isHostAuthorityRequest(message)) {
287
+ log("worker.host_request_unsupported", { method: message.method });
288
+ bridge.sendLine(unsupportedHostRequestResponse(message.id, message.method));
289
+ return;
290
+ }
291
+ if (!hubAuth &&
292
+ message?.method === "agent/approvalRequest" &&
293
+ replyUnsupportedLoopback(message.params))
294
+ return;
243
295
  if (hubAuth && message?.method === "agent/streamEvent") {
244
296
  const params = message.params;
245
297
  if (typeof params?.sessionId === "string" && params.event !== undefined) {
@@ -288,6 +340,8 @@ export async function startHeadlessServer(opts) {
288
340
  typeof approval.sessionId === "string") {
289
341
  if (automationWorker?.handleApproval(approval))
290
342
  return;
343
+ if (replyUnsupportedLoopback(approval))
344
+ return;
291
345
  const request = approval.request;
292
346
  if (request?.toolName === "__panel_action__") {
293
347
  const ownerId = runOwners.get(approval.sessionId);
@@ -391,6 +445,9 @@ export async function startHeadlessServer(opts) {
391
445
  buildEnv: () => ({
392
446
  ...process.env,
393
447
  CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
448
+ // These switch on Desktop-main-only IPC clients in the worker.
449
+ CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
450
+ CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
394
451
  CODE_SHELL_DATA_ROOT: workerDataRoot,
395
452
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
396
453
  ...(opts.workerCapabilityModules
@@ -502,6 +559,9 @@ export async function startHeadlessServer(opts) {
502
559
  buildEnv: () => ({
503
560
  ...process.env,
504
561
  CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
562
+ // These switch on Desktop-main-only IPC clients in the worker.
563
+ CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
564
+ CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
505
565
  CODE_SHELL_DATA_ROOT: workerDataRoot,
506
566
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
507
567
  ...(opts.workerCapabilityModules
@@ -591,7 +651,7 @@ export async function startHeadlessServer(opts) {
591
651
  res.setHeader("x-content-type-options", "nosniff");
592
652
  res.setHeader("referrer-policy", "no-referrer");
593
653
  if (pathname === "/health" && req.method === "GET") {
594
- hubJson(res, 200, { status: "ok", mode: hubAuth ? "hub" : "passcode" });
654
+ hubJson(res, 200, { status: "ok", mode: hubAuth ? "hub" : "passcode", contracts: CONTRACTS });
595
655
  return;
596
656
  }
597
657
  // The public shell probes this before login. A standalone workspace has no
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Worker → host requests that a headless serve host cannot fulfil.
3
+ *
4
+ * Desktop main answers the browser, credential-cookie and workspace-switch
5
+ * loopbacks plus the `desktop/*` authority requests. serve used to broadcast
6
+ * them to Web tabs as ordinary approvals, so the worker waited for its timeout
7
+ * (5 minutes for loopbacks, 30 seconds for automation). Answering at once with
8
+ * a typed `unsupported` result lets the tool fail fast with a reason.
9
+ *
10
+ * Pure and host-neutral on purpose: it moves into packages/host unchanged
11
+ * (architecture ch.16 §3.2 step 1.1). The wire shapes are documented in
12
+ * docs/todo/platform-s0-protocol-inventory.md ("S0 应答格式").
13
+ */
14
+ /** What this host can serve itself; Panel actions are only served with a Panel runtime. */
15
+ export interface ServeHostCapabilities {
16
+ panels: boolean;
17
+ }
18
+ export declare function unsupportedLoopbackCapability(toolName: unknown, host: ServeHostCapabilities): string | undefined;
19
+ /** The `answer` string for an approved loopback decision. */
20
+ export declare function unsupportedLoopbackAnswer(capability: string): string;
21
+ export declare function isHostAuthorityRequest(message: {
22
+ id?: unknown;
23
+ method?: unknown;
24
+ }): message is {
25
+ id: string | number;
26
+ method: string;
27
+ };
28
+ /** One JSON-RPC response line to write back to the worker. */
29
+ export declare function unsupportedHostRequestResponse(id: string | number, method: string): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Worker → host requests that a headless serve host cannot fulfil.
3
+ *
4
+ * Desktop main answers the browser, credential-cookie and workspace-switch
5
+ * loopbacks plus the `desktop/*` authority requests. serve used to broadcast
6
+ * them to Web tabs as ordinary approvals, so the worker waited for its timeout
7
+ * (5 minutes for loopbacks, 30 seconds for automation). Answering at once with
8
+ * a typed `unsupported` result lets the tool fail fast with a reason.
9
+ *
10
+ * Pure and host-neutral on purpose: it moves into packages/host unchanged
11
+ * (architecture ch.16 §3.2 step 1.1). The wire shapes are documented in
12
+ * docs/todo/platform-s0-protocol-inventory.md ("S0 应答格式").
13
+ */
14
+ const DESKTOP_ONLY_LOOPBACKS = Object.freeze({
15
+ __browser_action__: "browser",
16
+ __credential_action__: "credential.browserCookie",
17
+ __workspace_action__: "workspace.switch",
18
+ });
19
+ const PANEL_LOOPBACK = "__panel_action__";
20
+ const PANEL_CAPABILITY = "panel";
21
+ const HOST_AUTHORITY_METHOD_PREFIX = "desktop/";
22
+ const METHOD_NOT_FOUND = -32601;
23
+ function unsupportedText(capability) {
24
+ return `${capability} is not supported by this host (headless serve)`;
25
+ }
26
+ export function unsupportedLoopbackCapability(toolName, host) {
27
+ if (typeof toolName !== "string")
28
+ return undefined;
29
+ if (toolName === PANEL_LOOPBACK)
30
+ return host.panels ? undefined : PANEL_CAPABILITY;
31
+ return Object.hasOwn(DESKTOP_ONLY_LOOPBACKS, toolName)
32
+ ? DESKTOP_ONLY_LOOPBACKS[toolName]
33
+ : undefined;
34
+ }
35
+ /** The `answer` string for an approved loopback decision. */
36
+ export function unsupportedLoopbackAnswer(capability) {
37
+ const text = unsupportedText(capability);
38
+ return JSON.stringify({
39
+ ok: false,
40
+ failure: "unsupported",
41
+ capability,
42
+ error: text,
43
+ detail: text,
44
+ });
45
+ }
46
+ export function isHostAuthorityRequest(message) {
47
+ return ((typeof message.id === "string" || typeof message.id === "number") &&
48
+ typeof message.method === "string" &&
49
+ message.method.startsWith(HOST_AUTHORITY_METHOD_PREFIX));
50
+ }
51
+ /** One JSON-RPC response line to write back to the worker. */
52
+ export function unsupportedHostRequestResponse(id, method) {
53
+ return JSON.stringify({
54
+ jsonrpc: "2.0",
55
+ id,
56
+ error: {
57
+ code: METHOD_NOT_FOUND,
58
+ message: unsupportedText(method),
59
+ data: { failure: "unsupported", capability: method },
60
+ },
61
+ });
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.41",
3
+ "version": "0.9.42",
4
4
  "description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -61,13 +61,13 @@
61
61
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
62
62
  },
63
63
  "dependencies": {
64
- "@cjhyy/code-shell-capability-coding": "0.9.41",
65
- "@cjhyy/code-shell-core": "0.9.41",
66
- "@cjhyy/code-shell-web": "0.9.41",
64
+ "@cjhyy/code-shell-capability-coding": "0.9.42",
65
+ "@cjhyy/code-shell-core": "0.9.42",
66
+ "@cjhyy/code-shell-web": "0.9.42",
67
67
  "@modelcontextprotocol/sdk": "^1.31.0",
68
68
  "@types/ws": "^8.18.1",
69
69
  "ws": "^8.21.0",
70
- "@cjhyy/code-shell-link": "0.9.41"
70
+ "@cjhyy/code-shell-link": "0.9.42"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=20.10"