@cjhyy/code-shell-server 0.9.41 → 0.9.43

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.
@@ -80,7 +80,7 @@ export declare const REMOTE_LINK_ADAPTERS: ({
80
80
  id: "figma";
81
81
  name: "Figma";
82
82
  group: "files";
83
- actions: readonly ["get_file", "get_comments"];
83
+ actions: readonly ["get_file", "get_comments", "get_nodes", "get_images", "get_image_fills"];
84
84
  })[];
85
85
  export type RemoteProviderId = RemoteLinkProviderId;
86
86
  export interface RemoteProviderCapabilities {
@@ -1042,7 +1042,6 @@ export function createLinkService(options = {}) {
1042
1042
  !REMOTE_LINK_ADAPTERS.some((adapter) => adapter.id === input.providerId) ||
1043
1043
  (input.resourceUrl !== undefined &&
1044
1044
  (input.providerId !== "figma" ||
1045
- !input.connectionId ||
1046
1045
  typeof input.resourceUrl !== "string" ||
1047
1046
  !input.resourceUrl)))
1048
1047
  throw new LinkServiceError(400, "invalid_request");
@@ -1070,13 +1069,12 @@ export function createLinkService(options = {}) {
1070
1069
  throw new LinkServiceError(409, "conflict");
1071
1070
  }
1072
1071
  const prepared = { id, expected, input: { ...input, label: string(input.label) } };
1073
- if (input.resourceUrl !== undefined && !expected)
1074
- throw new LinkServiceError(400, "invalid_request");
1075
1072
  let attempt;
1076
1073
  try {
1077
1074
  let resourceFiles;
1078
1075
  if (input.providerId === "figma") {
1079
- // Recover the old whitelist from Host custody. Browser input can only suggest one new file.
1076
+ // A first connection may select the pasted file in the same OAuth consent.
1077
+ // Existing files still come exclusively from Host custody; input suggests one new file.
1080
1078
  const groups = expected?.meta?.linkResourceGroups;
1081
1079
  if (expected && (!groups || groups.length !== 1 || groups[0]?.id !== "files"))
1082
1080
  throw new Error("Invalid saved Figma resources");
@@ -1179,6 +1177,21 @@ export function createLinkService(options = {}) {
1179
1177
  throw new LinkServiceError(409, "conflict");
1180
1178
  if (matches.length === 1) {
1181
1179
  const expected = matches[0];
1180
+ if (credential.meta?.linkProvider === "figma") {
1181
+ const oldGroups = expected.meta?.linkResourceGroups;
1182
+ const newGroups = credential.meta?.linkResourceGroups;
1183
+ // A concurrent first connection never reviewed this saved grant.
1184
+ // Reuse only when the newly minted authority already covers it;
1185
+ // a local union cannot expand the remote token's file whitelist.
1186
+ if (expected.meta?.linkAccountId !== credential.meta?.linkAccountId ||
1187
+ oldGroups?.length !== 1 ||
1188
+ oldGroups[0]?.id !== "files" ||
1189
+ newGroups?.length !== 1 ||
1190
+ newGroups[0]?.id !== "files" ||
1191
+ oldGroups[0].items.some((item) => !newGroups[0].items.some((next) => next.id === item.id)) ||
1192
+ expected.meta?.linkCapabilityIds?.some((id) => !credential.meta?.linkCapabilityIds?.includes(id)))
1193
+ throw new LinkServiceError(409, "conflict");
1194
+ }
1182
1195
  prepared = { ...prepared, id: expected.id, expected };
1183
1196
  credential = { ...credential, id: expected.id, label: expected.label };
1184
1197
  }
@@ -1,3 +1,4 @@
1
+ import { HostRuntime, interceptHostRequest, unsupportedAskUserDecision, } from "@cjhyy/code-shell-host";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { createRequire } from "node:module";
3
4
  import { isAbsolute } from "node:path";
@@ -59,6 +60,10 @@ function progressFromEvent(event) {
59
60
  }
60
61
  /** Real independent Core workers shared by Desktop Web and standalone Hub. */
61
62
  export function createPanelAgentTaskHost(options = {}) {
63
+ const hostRuntime = new HostRuntime({ kind: "headless", label: "Panel agent task" }, [
64
+ { id: "panels", capabilities: options.onPanelAction ? ["panel"] : [] },
65
+ ]);
66
+ hostRuntime.start();
62
67
  const instances = new Map();
63
68
  let closed = false;
64
69
  const models = options.models ??
@@ -144,30 +149,36 @@ export function createPanelAgentTaskHost(options = {}) {
144
149
  ...(typeof params.connectionId === "string" ? { connectionId: params.connectionId } : {}),
145
150
  ...(Number.isInteger(params.generation) ? { generation: params.generation } : {}),
146
151
  };
147
- if (request.toolName.startsWith("__")) {
152
+ if (request.toolName === "__ask_user__") {
153
+ // The Panel runtime only has a yes/no confirmation UI. It cannot answer
154
+ // a question; Core retires async questions without injecting a user answer.
155
+ outcomeResult(await rpc(running, "agent/approve", {
156
+ sessionId: pending.sessionId,
157
+ requestId: pending.id,
158
+ connectionId: pending.connectionId,
159
+ generation: pending.generation,
160
+ decision: unsupportedAskUserDecision(hostRuntime.adapter.label),
161
+ }));
162
+ return;
163
+ }
164
+ if (request.toolName === "__panel_action__" && options.onPanelAction) {
148
165
  let decision;
149
- if (request.toolName === "__panel_action__" && options.onPanelAction) {
150
- try {
151
- const result = await options.onPanelAction(instance.scope, request.args ?? {});
152
- await assertAuthorized(instance);
153
- const answer = JSON.stringify(result);
154
- if (!answer || Buffer.byteLength(answer) > 512 * 1024)
155
- throw new Error("Panel callback returned an invalid or oversized result");
156
- decision = { approved: true, answer };
157
- }
158
- catch (error) {
159
- decision = {
160
- approved: false,
161
- failure: "unavailable",
162
- reason: String(error).slice(0, 500),
163
- };
164
- }
166
+ try {
167
+ const result = await options.onPanelAction(instance.scope, request.args ?? {});
168
+ await assertAuthorized(instance);
169
+ const answer = JSON.stringify(result);
170
+ if (!answer || Buffer.byteLength(answer) > 512 * 1024)
171
+ throw new Error("Panel callback returned an invalid or oversized result");
172
+ decision = { approved: true, answer };
165
173
  }
166
- else {
174
+ catch (error) {
167
175
  decision = {
168
- approved: false,
169
- failure: "unavailable",
170
- reason: "This task host does not provide that capability",
176
+ approved: true,
177
+ answer: JSON.stringify({
178
+ ok: false,
179
+ failure: "unavailable",
180
+ detail: String(error).slice(0, 500),
181
+ }),
171
182
  };
172
183
  }
173
184
  await assertAuthorized(instance);
@@ -258,12 +269,19 @@ export function createPanelAgentTaskHost(options = {}) {
258
269
  entryPath,
259
270
  execPath: options.execPath,
260
271
  fallbackCwd: () => scope.cwd,
261
- buildEnv: options.buildEnv ??
262
- (() => ({
272
+ buildEnv: () => ({
273
+ ...(options.buildEnv?.() ?? {
263
274
  ...process.env,
264
- CODE_SHELL_CREDENTIAL_ACCESS: "local",
265
275
  CODE_SHELL_CAPABILITY_MODULES: `${import.meta.resolve("@cjhyy/code-shell-capability-coding")}#createCodingModule`,
266
- })),
276
+ }),
277
+ // A task worker has no Desktop main bridge, even inside Desktop Web.
278
+ CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
279
+ CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
280
+ CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
281
+ CODE_SHELL_WORKSPACE_ACCESS: undefined,
282
+ CODE_SHELL_HOST_MCP_SERVERS: undefined,
283
+ CODE_SHELL_CREDENTIAL_ACCESS: "local",
284
+ }),
267
285
  onWorkerStarted: ({ pid }) => {
268
286
  running.pid = pid;
269
287
  },
@@ -278,13 +296,27 @@ export function createPanelAgentTaskHost(options = {}) {
278
296
  catch {
279
297
  return;
280
298
  }
299
+ if (interceptHostRequest(message, line, {
300
+ runtime: hostRuntime,
301
+ sendLine: (reply) => running.bridge.sendLine(reply),
302
+ approve: (params) => {
303
+ if (params.sessionId !== running.sessionId)
304
+ return;
305
+ void assertAuthorized(instance)
306
+ .then(() => rpc(running, "agent/approve", params))
307
+ .then(outcomeResult)
308
+ .catch(() => stopWorker(running));
309
+ },
310
+ }))
311
+ return;
281
312
  if (message.params?.sessionId !== input.sessionId)
282
313
  return;
283
314
  if (message.method === "agent/approvalRequest") {
284
315
  void handleWorkerApproval(instance, running, message.params).catch(() => stopWorker(running));
285
316
  }
286
317
  else if (message.method === "agent/approvalResolved") {
287
- instance.approvals.delete(message.params.requestId);
318
+ if (!instance.approvals.delete(message.params.requestId))
319
+ return;
288
320
  void deliver(instance, "agent.task.approvalResolved", {
289
321
  taskId: input.taskId,
290
322
  requestId: message.params.requestId,
@@ -432,7 +464,10 @@ export function createPanelAgentTaskHost(options = {}) {
432
464
  connectionId: pending.connectionId,
433
465
  generation: pending.generation,
434
466
  decision: input.approved
435
- ? { approved: true, scope: "once" }
467
+ ? {
468
+ approved: true,
469
+ scope: "once",
470
+ }
436
471
  : { approved: false, reason: "Declined by user" },
437
472
  }));
438
473
  instance.approvals.delete(pending.id);
@@ -451,6 +486,7 @@ export function createPanelAgentTaskHost(options = {}) {
451
486
  for (const id of instances.keys())
452
487
  revokeInstance(id);
453
488
  closed = true;
489
+ hostRuntime.stop();
454
490
  },
455
491
  activeTaskCount,
456
492
  };
@@ -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 { HostRuntime, interceptHostRequest } from "@cjhyy/code-shell-host";
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,26 @@ export async function startHeadlessServer(opts) {
226
227
  }
227
228
  }, pendingResponseReaperMs);
228
229
  pendingResponseReaper.unref?.();
230
+ const hostRuntime = new HostRuntime({ kind: "headless", label: "headless serve" }, [
231
+ { id: "panels", capabilities: hubAuth ? ["panel"] : [] },
232
+ ]);
233
+ hostRuntime.start();
234
+ const interceptWorkerRequest = (message, line) => interceptHostRequest(message, line, {
235
+ runtime: hostRuntime,
236
+ sendLine: (reply) => bridge.sendLine(reply),
237
+ approve: (params) => {
238
+ void bridge
239
+ .request("agent/approve", params, {
240
+ id: `unsupported-reply-${randomUUID()}`,
241
+ consume: true,
242
+ settleOnExit: true,
243
+ failFast: true,
244
+ timeoutMs: 5000,
245
+ meta: { origin: "host", producer: "serve-unsupported-loopback" },
246
+ })
247
+ .catch((error) => log("worker.loopback_unsupported_failed", { reason: String(error) }));
248
+ },
249
+ });
229
250
  // Notifications describe shared agent state and are broadcast. Correlated
230
251
  // JSON-RPC responses must return only to the tab that issued the request;
231
252
  // browser tabs all start their local counters at `web-1`, so broadcasting a
@@ -239,6 +260,8 @@ export async function startHeadlessServer(opts) {
239
260
  log("worker.frame_dropped", { reason: "not json", raw: previewLine(line) });
240
261
  return;
241
262
  }
263
+ if (interceptWorkerRequest(message, line))
264
+ return;
242
265
  if (!message || message.id === undefined || message.method !== undefined) {
243
266
  if (hubAuth && message?.method === "agent/streamEvent") {
244
267
  const params = message.params;
@@ -391,6 +414,11 @@ export async function startHeadlessServer(opts) {
391
414
  buildEnv: () => ({
392
415
  ...process.env,
393
416
  CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
417
+ // These switch on Desktop-main-only IPC clients in the worker.
418
+ CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
419
+ CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
420
+ CODE_SHELL_HOST_MCP_SERVERS: undefined,
421
+ CODE_SHELL_WORKSPACE_ACCESS: undefined,
394
422
  CODE_SHELL_DATA_ROOT: workerDataRoot,
395
423
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
396
424
  ...(opts.workerCapabilityModules
@@ -502,6 +530,11 @@ export async function startHeadlessServer(opts) {
502
530
  buildEnv: () => ({
503
531
  ...process.env,
504
532
  CODE_SHELL_REMOTE_LINK_CLIENT_SECRET: undefined,
533
+ // These switch on Desktop-main-only IPC clients in the worker.
534
+ CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
535
+ CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
536
+ CODE_SHELL_HOST_MCP_SERVERS: undefined,
537
+ CODE_SHELL_WORKSPACE_ACCESS: undefined,
505
538
  CODE_SHELL_DATA_ROOT: workerDataRoot,
506
539
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
507
540
  ...(opts.workerCapabilityModules
@@ -591,7 +624,7 @@ export async function startHeadlessServer(opts) {
591
624
  res.setHeader("x-content-type-options", "nosniff");
592
625
  res.setHeader("referrer-policy", "no-referrer");
593
626
  if (pathname === "/health" && req.method === "GET") {
594
- hubJson(res, 200, { status: "ok", mode: hubAuth ? "hub" : "passcode" });
627
+ hubJson(res, 200, { status: "ok", mode: hubAuth ? "hub" : "passcode", contracts: CONTRACTS });
595
628
  return;
596
629
  }
597
630
  // The public shell probes this before login. A standalone workspace has no
@@ -1104,6 +1137,7 @@ export async function startHeadlessServer(opts) {
1104
1137
  responses.clear();
1105
1138
  await panelAutomations?.close();
1106
1139
  clearInterval(pendingResponseReaper);
1140
+ hostRuntime.stop();
1107
1141
  if (authReaper)
1108
1142
  clearInterval(authReaper);
1109
1143
  await uploads?.close();
@@ -0,0 +1,2 @@
1
+ /** @deprecated Import the shared host dispatcher from @cjhyy/code-shell-host. */
2
+ export { isHostAuthorityRequest, unsupportedHostRequestResponse, unsupportedLoopbackAnswer, unsupportedLoopbackCapability, type ServeHostCapabilities, } from "@cjhyy/code-shell-host";
@@ -0,0 +1,2 @@
1
+ /** @deprecated Import the shared host dispatcher from @cjhyy/code-shell-host. */
2
+ export { isHostAuthorityRequest, unsupportedHostRequestResponse, unsupportedLoopbackAnswer, unsupportedLoopbackCapability, } from "@cjhyy/code-shell-host";
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.43",
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,14 @@
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.43",
65
+ "@cjhyy/code-shell-core": "0.9.43",
66
+ "@cjhyy/code-shell-web": "0.9.43",
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.43",
71
+ "@cjhyy/code-shell-host": "0.9.43"
71
72
  },
72
73
  "engines": {
73
74
  "node": ">=20.10"