@cjhyy/code-shell-server 0.9.42 → 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,15 +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
- // 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,
267
- CODE_SHELL_CREDENTIAL_ACCESS: "local",
268
275
  CODE_SHELL_CAPABILITY_MODULES: `${import.meta.resolve("@cjhyy/code-shell-capability-coding")}#createCodingModule`,
269
- })),
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
+ }),
270
285
  onWorkerStarted: ({ pid }) => {
271
286
  running.pid = pid;
272
287
  },
@@ -281,13 +296,27 @@ export function createPanelAgentTaskHost(options = {}) {
281
296
  catch {
282
297
  return;
283
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;
284
312
  if (message.params?.sessionId !== input.sessionId)
285
313
  return;
286
314
  if (message.method === "agent/approvalRequest") {
287
315
  void handleWorkerApproval(instance, running, message.params).catch(() => stopWorker(running));
288
316
  }
289
317
  else if (message.method === "agent/approvalResolved") {
290
- instance.approvals.delete(message.params.requestId);
318
+ if (!instance.approvals.delete(message.params.requestId))
319
+ return;
291
320
  void deliver(instance, "agent.task.approvalResolved", {
292
321
  taskId: input.taskId,
293
322
  requestId: message.params.requestId,
@@ -435,7 +464,10 @@ export function createPanelAgentTaskHost(options = {}) {
435
464
  connectionId: pending.connectionId,
436
465
  generation: pending.generation,
437
466
  decision: input.approved
438
- ? { approved: true, scope: "once" }
467
+ ? {
468
+ approved: true,
469
+ scope: "once",
470
+ }
439
471
  : { approved: false, reason: "Declined by user" },
440
472
  }));
441
473
  instance.approvals.delete(pending.id);
@@ -454,6 +486,7 @@ export function createPanelAgentTaskHost(options = {}) {
454
486
  for (const id of instances.keys())
455
487
  revokeInstance(id);
456
488
  closed = true;
489
+ hostRuntime.stop();
457
490
  },
458
491
  activeTaskCount,
459
492
  };
@@ -42,7 +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
+ import { HostRuntime, interceptHostRequest } from "@cjhyy/code-shell-host";
46
46
  export async function startHeadlessServer(opts) {
47
47
  const remoteLink = remoteLinkHostConfiguration(opts.remoteLink, opts.publicOrigin);
48
48
  if (remoteLink && opts.authMode !== "hub")
@@ -227,48 +227,26 @@ export async function startHeadlessServer(opts) {
227
227
  }
228
228
  }, pendingResponseReaperMs);
229
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
- };
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
+ });
272
250
  // Notifications describe shared agent state and are broadcast. Correlated
273
251
  // JSON-RPC responses must return only to the tab that issued the request;
274
252
  // browser tabs all start their local counters at `web-1`, so broadcasting a
@@ -282,16 +260,9 @@ export async function startHeadlessServer(opts) {
282
260
  log("worker.frame_dropped", { reason: "not json", raw: previewLine(line) });
283
261
  return;
284
262
  }
263
+ if (interceptWorkerRequest(message, line))
264
+ return;
285
265
  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;
295
266
  if (hubAuth && message?.method === "agent/streamEvent") {
296
267
  const params = message.params;
297
268
  if (typeof params?.sessionId === "string" && params.event !== undefined) {
@@ -340,8 +311,6 @@ export async function startHeadlessServer(opts) {
340
311
  typeof approval.sessionId === "string") {
341
312
  if (automationWorker?.handleApproval(approval))
342
313
  return;
343
- if (replyUnsupportedLoopback(approval))
344
- return;
345
314
  const request = approval.request;
346
315
  if (request?.toolName === "__panel_action__") {
347
316
  const ownerId = runOwners.get(approval.sessionId);
@@ -448,6 +417,8 @@ export async function startHeadlessServer(opts) {
448
417
  // These switch on Desktop-main-only IPC clients in the worker.
449
418
  CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
450
419
  CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
420
+ CODE_SHELL_HOST_MCP_SERVERS: undefined,
421
+ CODE_SHELL_WORKSPACE_ACCESS: undefined,
451
422
  CODE_SHELL_DATA_ROOT: workerDataRoot,
452
423
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
453
424
  ...(opts.workerCapabilityModules
@@ -562,6 +533,8 @@ export async function startHeadlessServer(opts) {
562
533
  // These switch on Desktop-main-only IPC clients in the worker.
563
534
  CODE_SHELL_OPERATION_BATCH_ACCESS: undefined,
564
535
  CODE_SHELL_MODEL_REQUEST_SIGNING: undefined,
536
+ CODE_SHELL_HOST_MCP_SERVERS: undefined,
537
+ CODE_SHELL_WORKSPACE_ACCESS: undefined,
565
538
  CODE_SHELL_DATA_ROOT: workerDataRoot,
566
539
  CODE_SHELL_CREDENTIAL_ACCESS: "local",
567
540
  ...(opts.workerCapabilityModules
@@ -1164,6 +1137,7 @@ export async function startHeadlessServer(opts) {
1164
1137
  responses.clear();
1165
1138
  await panelAutomations?.close();
1166
1139
  clearInterval(pendingResponseReaper);
1140
+ hostRuntime.stop();
1167
1141
  if (authReaper)
1168
1142
  clearInterval(authReaper);
1169
1143
  await uploads?.close();
@@ -1,29 +1,2 @@
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;
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";
@@ -1,62 +1,2 @@
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
- }
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.42",
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.42",
65
- "@cjhyy/code-shell-core": "0.9.42",
66
- "@cjhyy/code-shell-web": "0.9.42",
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.42"
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"