@cjhyy/code-shell-server 0.8.13 → 0.9.0

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.
@@ -2,17 +2,21 @@ import { markAttachmentsSent, type InputAttachmentMeta } from "../attachment-ser
2
2
  import { type MobileRunBridge } from "./mobile-run-dispatch.js";
3
3
  import type { MobileUploadService } from "./mobile-upload-service.js";
4
4
  import type { MobileAttachmentSummary, MobileImageAttachment, PermissionMode } from "./types.js";
5
+ import type { WorkerFrameMeta } from "../worker-bridge-core.js";
5
6
  type ChatTurnUploads = Pick<MobileUploadService, "claim" | "release" | "finalize">;
6
7
  export interface DispatchMobileChatTurnInput {
7
8
  deviceId: string;
8
9
  sessionId: string;
9
10
  fallbackCwd: string;
11
+ projectId?: string;
12
+ rootId?: string;
10
13
  text: string;
11
14
  attachments?: MobileImageAttachment[];
12
15
  clientMessageId?: string;
13
16
  permissionMode?: PermissionMode;
14
17
  runId: string;
15
18
  bridge: MobileRunBridge;
19
+ meta: WorkerFrameMeta;
16
20
  uploads: ChatTurnUploads;
17
21
  resolveWorkspace: (sessionId: string, fallbackCwd: string) => Promise<string>;
18
22
  markSent?: typeof markAttachmentsSent;
@@ -46,11 +46,13 @@ export async function dispatchMobileChatTurn(input) {
46
46
  task: text,
47
47
  cwd,
48
48
  sessionId: input.sessionId,
49
+ ...(input.projectId ? { projectId: input.projectId } : {}),
50
+ ...(input.rootId ? { rootId: input.rootId } : {}),
49
51
  clientMessageId,
50
52
  attachments: materialized.metas,
51
53
  ...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
52
54
  },
53
- });
55
+ }, input.meta);
54
56
  if (!acceptance.ok) {
55
57
  await settleClaims(input.uploads, input.deviceId, materialized.claims, "release");
56
58
  return { ok: false, message: acceptance.message };
@@ -46,9 +46,13 @@ export function parseMobileClientEvent(value) {
46
46
  break;
47
47
  case "session.create":
48
48
  valid =
49
- (event.cwd === undefined ||
50
- event.cwd === null ||
51
- boundedPath(event.cwd)) &&
49
+ (event.cwd === undefined || event.cwd === null || boundedPath(event.cwd)) &&
50
+ (event.projectId === undefined ||
51
+ event.projectId === null ||
52
+ boundedString(event.projectId, MAX_ID_LENGTH)) &&
53
+ optionalBoundedString(event.rootId, MAX_ID_LENGTH) &&
54
+ (event.rootId === undefined || typeof event.projectId === "string") &&
55
+ !(event.projectId === null && event.rootId !== undefined) &&
52
56
  optionalBoundedString(event.name, MAX_NAME_LENGTH);
53
57
  break;
54
58
  case "run.stop":
@@ -71,8 +75,7 @@ export function parseMobileClientEvent(value) {
71
75
  break;
72
76
  case "session.sync":
73
77
  valid =
74
- boundedString(event.sessionId, MAX_ID_LENGTH) &&
75
- optionalNonNegativeInteger(event.sinceSeq);
78
+ boundedString(event.sessionId, MAX_ID_LENGTH) && optionalNonNegativeInteger(event.sinceSeq);
76
79
  break;
77
80
  case "permission.setMode":
78
81
  valid =
@@ -114,8 +117,7 @@ export function parseMobileClientEvent(value) {
114
117
  break;
115
118
  case "room.history":
116
119
  valid =
117
- boundedString(event.roomId, MAX_ID_LENGTH) &&
118
- optionalNonNegativeInteger(event.sinceSeq);
120
+ boundedString(event.roomId, MAX_ID_LENGTH) && optionalNonNegativeInteger(event.sinceSeq);
119
121
  break;
120
122
  case "ccRoom.probe":
121
123
  valid =
@@ -202,9 +204,7 @@ function isBoundedHistoryLimit(value) {
202
204
  }
203
205
  function optionalAttachments(value) {
204
206
  return (value === undefined ||
205
- (Array.isArray(value) &&
206
- value.length <= MAX_MOBILE_ATTACHMENTS &&
207
- value.every(isAttachment)));
207
+ (Array.isArray(value) && value.length <= MAX_MOBILE_ATTACHMENTS && value.every(isAttachment)));
208
208
  }
209
209
  function isAttachment(value) {
210
210
  if (!isRecord(value) ||
@@ -223,8 +223,9 @@ function isApprovalDecision(value) {
223
223
  if (!isRecord(value))
224
224
  return false;
225
225
  if (value.behavior === "allow") {
226
- return ((value.answer === undefined || boundedOptionalEmptyString(value.answer, MAX_MESSAGE_LENGTH)) &&
226
+ return ((value.answer === undefined ||
227
+ boundedOptionalEmptyString(value.answer, MAX_MESSAGE_LENGTH)) &&
227
228
  (value.updatedInput === undefined || isRecord(value.updatedInput)));
228
229
  }
229
- return (value.behavior === "deny" && boundedOptionalEmptyString(value.message, MAX_MESSAGE_LENGTH));
230
+ return value.behavior === "deny" && boundedOptionalEmptyString(value.message, MAX_MESSAGE_LENGTH);
230
231
  }
@@ -1,6 +1,7 @@
1
+ import type { WorkerFrameMeta } from "../worker-bridge-core.js";
1
2
  export interface MobileRunBridge {
2
3
  subscribeOutbound(listener: (line: string) => void): () => void;
3
- injectWorkerMessage(line: string): void;
4
+ injectWorkerMessage(line: string, meta: WorkerFrameMeta): void;
4
5
  }
5
6
  export interface MobileRunRequest {
6
7
  id: string;
@@ -14,4 +15,4 @@ export type MobileRunAcceptance = {
14
15
  code?: number;
15
16
  };
16
17
  /** Inject agent/run and wait for its request-correlated validation/queue acknowledgement. */
17
- export declare function injectMobileRunAndAwaitAcceptance(bridge: MobileRunBridge, request: MobileRunRequest, timeoutMs?: number): Promise<MobileRunAcceptance>;
18
+ export declare function injectMobileRunAndAwaitAcceptance(bridge: MobileRunBridge, request: MobileRunRequest, meta: WorkerFrameMeta, timeoutMs?: number): Promise<MobileRunAcceptance>;
@@ -1,5 +1,5 @@
1
1
  /** Inject agent/run and wait for its request-correlated validation/queue acknowledgement. */
2
- export function injectMobileRunAndAwaitAcceptance(bridge, request, timeoutMs = 5_000) {
2
+ export function injectMobileRunAndAwaitAcceptance(bridge, request, meta, timeoutMs = 5_000) {
3
3
  return new Promise((resolve) => {
4
4
  let settled = false;
5
5
  let timer;
@@ -47,7 +47,7 @@ export function injectMobileRunAndAwaitAcceptance(bridge, request, timeoutMs = 5
47
47
  id: request.id,
48
48
  method: "agent/run",
49
49
  params: request.params,
50
- }));
50
+ }), meta);
51
51
  }
52
52
  catch (error) {
53
53
  finish({ ok: false, message: error instanceof Error ? error.message : String(error) });
@@ -38,8 +38,11 @@ export class TrustedDeviceStore {
38
38
  // Get-or-create by secretHash. A phone's secret is stable per browser
39
39
  // (persisted in localStorage as cs.deviceSecret), so re-scanning the QR
40
40
  // code must reuse its live row rather than accumulating duplicates.
41
- const existing = devices.find((d) => d.secretHash === input.secretHash && !d.revokedAt);
41
+ const existing = devices.find((d) => secretHashEquals(d.secretHash, input.secretHash));
42
42
  if (existing) {
43
+ if (existing.revokedAt) {
44
+ throw new Error("Trusted device was revoked; remove it before pairing again");
45
+ }
43
46
  existing.name = name;
44
47
  existing.lastSeenAt = Date.now();
45
48
  return { result: this.toPublic(existing), changed: true };
@@ -232,7 +232,7 @@ export async function startHeadlessServer(opts) {
232
232
  // Spawn-on-first-frame (idempotent): the browser's first request wakes
233
233
  // the worker, mirroring the renderer's spawn-on-agent/run semantics.
234
234
  bridge.ensureWorker(opts.cwd);
235
- bridge.injectWorkerMessage(workerLine);
235
+ bridge.injectWorkerMessage(workerLine, { origin: "serve", producer: "serve-ws" });
236
236
  });
237
237
  ws.on("close", () => {
238
238
  tabs.delete(ws);
@@ -1,6 +1,11 @@
1
1
  /** Truncate a wire line for logs. */
2
2
  export declare function previewLine(s: string, max?: number): string;
3
3
  export type WorkerBridgeLog = (event: string, data?: Record<string, unknown>) => void;
4
+ export interface WorkerFrameMeta {
5
+ origin: "renderer" | "mobile" | "host" | "serve";
6
+ /** Producer label for logs and audits. It is never an authorization input. */
7
+ producer: string;
8
+ }
4
9
  export interface WorkerExitInfo {
5
10
  code: number | null;
6
11
  signal: NodeJS.Signals | null;
@@ -55,6 +60,8 @@ export interface WorkerRequestOptions {
55
60
  ensureWorker?: boolean;
56
61
  /** cwd handed to ensureWorker() when it spawns. Ignored unless ensureWorker. */
57
62
  ensureWorkerCwd?: string;
63
+ /** Main-owned provenance for this frame. */
64
+ meta: WorkerFrameMeta;
58
65
  }
59
66
  export interface WorkerBridgeCoreOptions {
60
67
  /** Absolute path of the worker entry script (agent-server-stdio). */
@@ -71,7 +78,7 @@ export interface WorkerBridgeCoreOptions {
71
78
  * injected or correlated (e.g. agent/run trust metadata + on-demand spawn).
72
79
  * Returns the line to write; `method` is only used for log context.
73
80
  */
74
- prepareInbound?: (line: string) => {
81
+ prepareInbound?: (line: string, meta: WorkerFrameMeta) => {
75
82
  line: string;
76
83
  method?: string;
77
84
  };
@@ -141,7 +148,7 @@ export declare class WorkerBridgeCore {
141
148
  * written, or dropped with a log when no worker is alive — identical
142
149
  * semantics for every front end (renderer, mobile remote, orchestrators).
143
150
  */
144
- injectWorkerMessage(rawLine: string): void;
151
+ injectWorkerMessage(rawLine: string, meta: WorkerFrameMeta): void;
145
152
  /**
146
153
  * Send a correlated JSON-RPC request and await its response by id.
147
154
  * `params` is omitted from the frame when undefined. Never rejects — the
@@ -200,9 +200,7 @@ export class WorkerBridgeCore {
200
200
  const pending = this.pendingRequests.get(msg.id);
201
201
  if (!pending)
202
202
  return false;
203
- pending.settle(msg.error
204
- ? { status: "error", error: msg.error }
205
- : { status: "result", result: msg.result });
203
+ pending.settle(msg.error ? { status: "error", error: msg.error } : { status: "result", result: msg.result });
206
204
  return pending.consume;
207
205
  }
208
206
  settlePendingOnExit() {
@@ -239,18 +237,25 @@ export class WorkerBridgeCore {
239
237
  * written, or dropped with a log when no worker is alive — identical
240
238
  * semantics for every front end (renderer, mobile remote, orchestrators).
241
239
  */
242
- injectWorkerMessage(rawLine) {
240
+ injectWorkerMessage(rawLine, meta) {
243
241
  const prep = this.opts.prepareInbound
244
- ? this.opts.prepareInbound(rawLine)
242
+ ? this.opts.prepareInbound(rawLine, meta)
245
243
  : { line: rawLine, method: undefined };
246
244
  if (!this.canSend()) {
247
245
  this.log("inject.dropped", {
248
246
  reason: this.child ? "stdin destroyed" : "no child",
249
247
  method: prep.method,
248
+ origin: meta.origin,
249
+ producer: meta.producer,
250
250
  });
251
251
  return;
252
252
  }
253
- this.log("inject→worker", { method: prep.method, raw: previewLine(prep.line) });
253
+ this.log("inject→worker", {
254
+ method: prep.method,
255
+ origin: meta.origin,
256
+ producer: meta.producer,
257
+ raw: previewLine(prep.line),
258
+ });
254
259
  this.sendLine(prep.line);
255
260
  }
256
261
  /**
@@ -298,7 +303,7 @@ export class WorkerBridgeCore {
298
303
  // bypassing this hook left the main process unable to route the first
299
304
  // Panel tool call for a restored session.
300
305
  const prepared = this.opts.prepareInbound
301
- ? this.opts.prepareInbound(rawFrame)
306
+ ? this.opts.prepareInbound(rawFrame, options.meta)
302
307
  : { line: rawFrame, method: undefined };
303
308
  // Wake a lazily-unspawned worker before writing, so a spawn-triggering
304
309
  // request (pet/IM-gateway agent/run) isn't dropped by sendLine and left
@@ -311,6 +316,17 @@ export class WorkerBridgeCore {
311
316
  catch (e) {
312
317
  sendError = e;
313
318
  }
319
+ const rejected = sendError;
320
+ if (typeof rejected?.code === "number") {
321
+ settle({
322
+ status: "error",
323
+ error: {
324
+ code: rejected.code,
325
+ message: typeof rejected.message === "string" ? rejected.message : "worker frame rejected",
326
+ },
327
+ });
328
+ return;
329
+ }
314
330
  if (options.failFast && (!sent || sendError !== undefined)) {
315
331
  settle({ status: "sendFailed", error: sendError });
316
332
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.8.13",
3
+ "version": "0.9.0",
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",
@@ -39,9 +39,9 @@
39
39
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
40
40
  },
41
41
  "dependencies": {
42
- "@cjhyy/code-shell-capability-coding": "0.8.13",
43
- "@cjhyy/code-shell-core": "0.8.13",
44
- "@cjhyy/code-shell-web": "0.8.13",
42
+ "@cjhyy/code-shell-capability-coding": "0.9.0",
43
+ "@cjhyy/code-shell-core": "0.9.0",
44
+ "@cjhyy/code-shell-web": "0.9.0",
45
45
  "@types/ws": "^8.18.1",
46
46
  "ws": "^8.21.0"
47
47
  },