@cjhyy/code-shell-server 0.9.51 → 0.10.1

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 (93) hide show
  1. package/dist/account/cli.d.ts +23 -0
  2. package/dist/account/cli.js +249 -0
  3. package/dist/account/cloud-contract.d.ts +39 -0
  4. package/dist/account/cloud-contract.js +134 -0
  5. package/dist/account/cloud-types.d.ts +47 -0
  6. package/dist/account/cloud-types.js +1 -0
  7. package/dist/account/feature-service.d.ts +47 -0
  8. package/dist/account/feature-service.js +184 -0
  9. package/dist/account/http.d.ts +17 -0
  10. package/dist/account/http.js +158 -0
  11. package/dist/account/links.d.ts +13 -0
  12. package/dist/account/links.js +137 -0
  13. package/dist/account/manager.d.ts +96 -0
  14. package/dist/account/manager.js +976 -0
  15. package/dist/account/native-lock.d.ts +6 -0
  16. package/dist/account/native-lock.js +26 -0
  17. package/dist/account/native-store.d.ts +32 -0
  18. package/dist/account/native-store.js +162 -0
  19. package/dist/account/profile.d.ts +4 -0
  20. package/dist/account/profile.js +46 -0
  21. package/dist/account/store.d.ts +34 -0
  22. package/dist/account/store.js +118 -0
  23. package/dist/account/types.d.ts +138 -0
  24. package/dist/account/types.js +2 -0
  25. package/dist/desktop-web/http-api.d.ts +4 -0
  26. package/dist/desktop-web/http-api.js +12 -4
  27. package/dist/environment-identity.d.ts +1 -1
  28. package/dist/environment-identity.js +2 -1
  29. package/dist/headless-worker-env.d.ts +2 -0
  30. package/dist/headless-worker-env.js +15 -0
  31. package/dist/hub/approval-lease.js +6 -1
  32. package/dist/hub/configuration.d.ts +11 -0
  33. package/dist/hub/configuration.js +34 -1
  34. package/dist/hub/model-parameter-metadata.d.ts +26 -0
  35. package/dist/hub/model-parameter-metadata.js +92 -0
  36. package/dist/hub/run-replay.js +1 -1
  37. package/dist/hub/session-management.js +1 -1
  38. package/dist/index.account.d.ts +12 -0
  39. package/dist/index.account.js +12 -0
  40. package/dist/index.mobile-remote.d.ts +2 -0
  41. package/dist/index.mobile-remote.js +1 -0
  42. package/dist/index.worker.d.ts +1 -0
  43. package/dist/index.worker.js +1 -0
  44. package/dist/mobile-remote/account-relay-access.d.ts +59 -0
  45. package/dist/mobile-remote/account-relay-access.js +174 -0
  46. package/dist/mobile-remote/approval-clock.d.ts +3 -0
  47. package/dist/mobile-remote/approval-clock.js +15 -0
  48. package/dist/mobile-remote/codex-room-agent.d.ts +6 -0
  49. package/dist/mobile-remote/codex-room-agent.js +17 -1
  50. package/dist/mobile-remote/mobile-chat-turn.d.ts +4 -0
  51. package/dist/mobile-remote/mobile-chat-turn.js +3 -0
  52. package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
  53. package/dist/mobile-remote/mobile-upload-service.d.ts +9 -0
  54. package/dist/mobile-remote/mobile-upload-service.js +25 -0
  55. package/dist/mobile-remote/remote-host-manager.d.ts +21 -4
  56. package/dist/mobile-remote/remote-host-manager.js +142 -25
  57. package/dist/mobile-remote/resident-agent.d.ts +14 -0
  58. package/dist/mobile-remote/resident-agent.js +38 -11
  59. package/dist/mobile-remote/room-history.d.ts +7 -0
  60. package/dist/mobile-remote/room-history.js +56 -1
  61. package/dist/mobile-remote/room-manager.d.ts +216 -9
  62. package/dist/mobile-remote/room-manager.js +700 -65
  63. package/dist/mobile-remote/room-process-stop.d.ts +15 -0
  64. package/dist/mobile-remote/room-process-stop.js +41 -0
  65. package/dist/mobile-remote/trusted-device-store.d.ts +12 -0
  66. package/dist/mobile-remote/trusted-device-store.js +54 -1
  67. package/dist/mobile-remote/turn-queue.d.ts +183 -0
  68. package/dist/mobile-remote/turn-queue.js +422 -0
  69. package/dist/mobile-remote/types.d.ts +1 -1
  70. package/dist/panels/agent-task-host.js +6 -12
  71. package/dist/panels/agent-task-service.d.ts +2 -0
  72. package/dist/panels/agent-task-service.js +7 -0
  73. package/dist/panels/browser-capture.d.ts +1 -15
  74. package/dist/panels/browser-capture.js +1 -35
  75. package/dist/panels/directory-picker.d.ts +48 -0
  76. package/dist/panels/directory-picker.js +247 -0
  77. package/dist/panels/http.d.ts +15 -15
  78. package/dist/panels/management-http.d.ts +9 -9
  79. package/dist/panels/process-service.d.ts +5 -4
  80. package/dist/panels/process-service.js +54 -16
  81. package/dist/panels/runtime.js +148 -30
  82. package/dist/panels/tool-jobs-storage.d.ts +4 -0
  83. package/dist/panels/tool-jobs-storage.js +111 -34
  84. package/dist/panels/types.d.ts +1 -97
  85. package/dist/remote-relay/connector.d.ts +8 -0
  86. package/dist/remote-relay/connector.js +29 -2
  87. package/dist/remote-relay/protocol.d.ts +21 -0
  88. package/dist/remote-relay/protocol.js +31 -0
  89. package/dist/serve/cli.js +6 -0
  90. package/dist/serve/headless-server.js +24 -19
  91. package/dist/sessions-service.d.ts +4 -0
  92. package/dist/sessions-service.js +14 -1
  93. package/package.json +10 -6
@@ -1,6 +1,21 @@
1
1
  import type { ChildProcess } from "node:child_process";
2
2
  /** Retire an unusable room transport without leaving an unresponsive CLI alive. */
3
3
  export declare function stopRoomProcess(child: ChildProcess): void;
4
+ /**
5
+ * Resolve true once `child` has exited (or never started a process), false
6
+ * when it is still alive after `timeoutMs`. A replacement CLI for the same
7
+ * external session may only start after this resolves true; otherwise two
8
+ * processes could append to one transcript.
9
+ */
10
+ export declare function waitForRoomProcessExit(child: ChildProcess, timeoutMs: number): Promise<boolean>;
11
+ /** Signal the process group, then wait (bounded) for the child to exit. */
12
+ export declare function stopRoomProcessAndWait(child: ChildProcess, timeoutMs: number): Promise<boolean>;
13
+ /**
14
+ * Resolve once a just-spawned child exists. Node assigns `pid` synchronously
15
+ * for a successful spawn; a failed spawn (missing command/cwd) has no pid and
16
+ * reports the failure through `error` on a later tick.
17
+ */
18
+ export declare function waitForRoomProcessSpawn(child: ChildProcess): Promise<void>;
4
19
  /**
5
20
  * `close` waits for every holder of the child's stdout/stderr. A grandchild the
6
21
  * CLI backgrounded with inherited stdio (a dev server, say) keeps them open
@@ -26,6 +26,47 @@ export function stopRoomProcess(child) {
26
26
  timer.unref();
27
27
  child.once("exit", () => clearTimeout(timer));
28
28
  }
29
+ /**
30
+ * Resolve true once `child` has exited (or never started a process), false
31
+ * when it is still alive after `timeoutMs`. A replacement CLI for the same
32
+ * external session may only start after this resolves true; otherwise two
33
+ * processes could append to one transcript.
34
+ */
35
+ export function waitForRoomProcessExit(child, timeoutMs) {
36
+ if (child.exitCode !== null || child.signalCode !== null || child.pid === undefined)
37
+ return Promise.resolve(true);
38
+ return new Promise((resolve) => {
39
+ const onExit = () => {
40
+ clearTimeout(timer);
41
+ resolve(true);
42
+ };
43
+ const timer = setTimeout(() => {
44
+ child.off("exit", onExit);
45
+ resolve(false);
46
+ }, timeoutMs);
47
+ timer.unref?.();
48
+ child.once("exit", onExit);
49
+ });
50
+ }
51
+ /** Signal the process group, then wait (bounded) for the child to exit. */
52
+ export function stopRoomProcessAndWait(child, timeoutMs) {
53
+ const exited = waitForRoomProcessExit(child, timeoutMs);
54
+ stopRoomProcess(child);
55
+ return exited;
56
+ }
57
+ /**
58
+ * Resolve once a just-spawned child exists. Node assigns `pid` synchronously
59
+ * for a successful spawn; a failed spawn (missing command/cwd) has no pid and
60
+ * reports the failure through `error` on a later tick.
61
+ */
62
+ export function waitForRoomProcessSpawn(child) {
63
+ if (child.pid !== undefined)
64
+ return Promise.resolve();
65
+ return new Promise((resolve, reject) => {
66
+ child.once("spawn", () => resolve());
67
+ child.once("error", (error) => reject(error));
68
+ });
69
+ }
29
70
  /**
30
71
  * `close` waits for every holder of the child's stdout/stderr. A grandchild the
31
72
  * CLI backgrounded with inherited stdio (a dev server, say) keeps them open
@@ -1,7 +1,19 @@
1
1
  import type { TrustedDevicePublic } from "./types.js";
2
2
  export declare class TrustedDeviceStore {
3
3
  private readonly filePath;
4
+ /** Account handoffs never create durable pairing credentials, even after a crash. */
5
+ private readonly ephemeral;
4
6
  constructor(filePath: string);
7
+ addEphemeralDevice(input: {
8
+ name: string;
9
+ secretHash: string;
10
+ expiresAt: number;
11
+ }): TrustedDevicePublic;
12
+ private pruneEphemeral;
13
+ /** Account identities require their matching entrance grant on every transport. */
14
+ isEphemeralDevice(id: string): boolean;
15
+ /** Retiring an account grant must never acquire or mutate the durable pairing store. */
16
+ removeEphemeralDevice(id: string): boolean;
5
17
  addDevice(input: {
6
18
  name: string;
7
19
  secretHash: string;
@@ -24,9 +24,46 @@ function secretHashEquals(a, b) {
24
24
  }
25
25
  export class TrustedDeviceStore {
26
26
  filePath;
27
+ /** Account handoffs never create durable pairing credentials, even after a crash. */
28
+ ephemeral = new Map();
27
29
  constructor(filePath) {
28
30
  this.filePath = filePath;
29
31
  }
32
+ addEphemeralDevice(input) {
33
+ const name = normalizeDeviceName(input.name);
34
+ if (!name ||
35
+ !isBoundedOpaqueString(input.secretHash, MAX_DEVICE_SECRET_LENGTH) ||
36
+ !isTimestamp(input.expiresAt) ||
37
+ input.expiresAt <= Date.now() ||
38
+ input.expiresAt > Date.now() + 10 * 60_000)
39
+ throw new Error("Invalid short-lived device grant");
40
+ this.pruneEphemeral();
41
+ if (this.ephemeral.size >= 128)
42
+ throw new Error("Short-lived device limit reached");
43
+ const device = {
44
+ id: randomUUID(),
45
+ name,
46
+ secretHash: input.secretHash,
47
+ createdAt: Date.now(),
48
+ };
49
+ this.ephemeral.set(device.id, { device, expiresAt: input.expiresAt });
50
+ return this.toPublic(device);
51
+ }
52
+ pruneEphemeral() {
53
+ const now = Date.now();
54
+ for (const [id, grant] of this.ephemeral)
55
+ if (grant.expiresAt <= now)
56
+ this.ephemeral.delete(id);
57
+ }
58
+ /** Account identities require their matching entrance grant on every transport. */
59
+ isEphemeralDevice(id) {
60
+ this.pruneEphemeral();
61
+ return this.ephemeral.has(id);
62
+ }
63
+ /** Retiring an account grant must never acquire or mutate the durable pairing store. */
64
+ removeEphemeralDevice(id) {
65
+ return this.ephemeral.delete(id);
66
+ }
30
67
  addDevice(input) {
31
68
  const name = normalizeDeviceName(input.name);
32
69
  if (!name)
@@ -34,6 +71,9 @@ export class TrustedDeviceStore {
34
71
  if (!isBoundedOpaqueString(input.secretHash, MAX_DEVICE_SECRET_LENGTH)) {
35
72
  throw new Error("Invalid trusted device secret");
36
73
  }
74
+ this.pruneEphemeral();
75
+ if ([...this.ephemeral.values()].some(({ device }) => secretHashEquals(device.secretHash, input.secretHash)))
76
+ throw new Error("Account device credentials cannot be paired permanently");
37
77
  return this.mutate((devices) => {
38
78
  // Get-or-create by secretHash. A phone's secret is stable per browser
39
79
  // (persisted in localStorage as cs.deviceSecret), so re-scanning the QR
@@ -61,13 +101,22 @@ export class TrustedDeviceStore {
61
101
  });
62
102
  }
63
103
  listDevices() {
64
- return this.readAll().map((device) => this.toPublic(device));
104
+ this.pruneEphemeral();
105
+ return [...this.readAll(), ...[...this.ephemeral.values()].map((grant) => grant.device)].map((device) => this.toPublic(device));
65
106
  }
66
107
  authenticate(id, secretHash) {
67
108
  if (!isBoundedOpaqueString(id, MAX_DEVICE_ID_LENGTH) ||
68
109
  !isBoundedOpaqueString(secretHash, MAX_DEVICE_SECRET_LENGTH)) {
69
110
  return undefined;
70
111
  }
112
+ this.pruneEphemeral();
113
+ const temporary = this.ephemeral.get(id)?.device;
114
+ if (temporary) {
115
+ if (temporary.revokedAt || !secretHashEquals(temporary.secretHash, secretHash))
116
+ return;
117
+ temporary.lastSeenAt = Date.now();
118
+ return this.toPublic(temporary);
119
+ }
71
120
  return this.mutate((devices) => {
72
121
  const device = devices.find((item) => item.id === id && !item.revokedAt && secretHashEquals(item.secretHash, secretHash));
73
122
  if (!device)
@@ -79,6 +128,8 @@ export class TrustedDeviceStore {
79
128
  revoke(id) {
80
129
  if (!isBoundedOpaqueString(id, MAX_DEVICE_ID_LENGTH))
81
130
  return false;
131
+ if (this.ephemeral.has(id))
132
+ return this.ephemeral.delete(id);
82
133
  return this.mutate((devices) => {
83
134
  const device = devices.find((item) => item.id === id && !item.revokedAt);
84
135
  if (!device)
@@ -91,6 +142,8 @@ export class TrustedDeviceStore {
91
142
  remove(id) {
92
143
  if (!isBoundedOpaqueString(id, MAX_DEVICE_ID_LENGTH))
93
144
  return false;
145
+ if (this.ephemeral.has(id))
146
+ return this.ephemeral.delete(id);
94
147
  return this.mutate((devices) => {
95
148
  const index = devices.findIndex((item) => item.id === id);
96
149
  if (index < 0)
@@ -0,0 +1,183 @@
1
+ import type { MobileAttachmentSummary, MobileTurnQueueEntryStatus, MobileTurnQueueState, MobileTurnQueueTarget, PermissionMode } from "./types.js";
2
+ /**
3
+ * Host-owned FIFO of turns submitted while their conversation is busy.
4
+ *
5
+ * The queue never runs anything itself: an entry is handed to the caller's
6
+ * ORIGINAL submit path (same validation, approvals and workspace checks) only
7
+ * when the exact target is idle, so it cannot bypass or duplicate an existing
8
+ * Core/CLI queue. Entries are bound to the conversation they were submitted
9
+ * for, never to a viewer's later selection.
10
+ */
11
+ export declare const MAX_TURN_QUEUE_ENTRIES_PER_TARGET = 20;
12
+ export declare const MAX_TURN_QUEUE_ENTRIES_PER_DEVICE = 50;
13
+ export declare const MAX_TURN_QUEUE_ENTRIES = 200;
14
+ export type TurnQueueDispatchResult = {
15
+ ok: true;
16
+ }
17
+ /**
18
+ * requeue: a Stop of the target fenced the turn off before anything was
19
+ * submitted; it returns to the (stopped) queue unchanged, never failed.
20
+ */
21
+ | {
22
+ ok: false;
23
+ message: string;
24
+ uncertain?: boolean;
25
+ requeue?: boolean;
26
+ };
27
+ export type TurnQueueActionResult = {
28
+ ok: true;
29
+ } | {
30
+ ok: false;
31
+ message: string;
32
+ };
33
+ export type TurnQueueGate = {
34
+ ok: true;
35
+ viewerId?: string;
36
+ } | {
37
+ ok: false;
38
+ message: string;
39
+ };
40
+ export interface TurnQueueSubmission<P> {
41
+ target: MobileTurnQueueTarget;
42
+ clientMessageId: string;
43
+ deviceId: string;
44
+ viewerId?: string;
45
+ text: string;
46
+ attachments: MobileAttachmentSummary[];
47
+ uploadIds: string[];
48
+ permissionMode?: PermissionMode;
49
+ modelKey?: string;
50
+ nativeConnectionId?: string;
51
+ /** The original validated client event, replayed through its own submit path. */
52
+ payload: P;
53
+ /**
54
+ * Input identity (text + attachment descriptors). A replay of the same intent
55
+ * id with different content is refused rather than mistaken for the same turn.
56
+ */
57
+ fingerprint?: string;
58
+ /**
59
+ * The submitter still awaits a direct acknowledgement (an immediate run on
60
+ * an idle target). Hidden from snapshots and dropped on rejection, so the
61
+ * original submit path's error keeps its legacy meaning. Cleared by reveal().
62
+ */
63
+ transient?: boolean;
64
+ }
65
+ export interface TurnQueueEntry<P> extends TurnQueueSubmission<P> {
66
+ id: string;
67
+ createdAt: number;
68
+ status: MobileTurnQueueEntryStatus;
69
+ message?: string;
70
+ }
71
+ export interface MobileTurnQueueDeps<P> {
72
+ /** True while the exact target has an active turn (including its approvals). */
73
+ isBusy(target: MobileTurnQueueTarget): boolean;
74
+ /** Re-check the submitting device's authority immediately before running. */
75
+ canDispatch(entry: Readonly<TurnQueueEntry<P>>): TurnQueueGate;
76
+ dispatch(entry: Readonly<TurnQueueEntry<P>>): Promise<TurnQueueDispatchResult>;
77
+ /** Called after every visible change of one target's queue. */
78
+ publish(target: MobileTurnQueueTarget): void;
79
+ /** Release resources (uploads) of an entry that will never run. */
80
+ discard?(entry: Readonly<TurnQueueEntry<P>>): void;
81
+ now?: () => number;
82
+ /** Poll interval for idle detection; 0 disables the internal timer (tests). */
83
+ intervalMs?: number;
84
+ /** Minimum gap after an accepted dispatch before the next head may run. */
85
+ settleMs?: number;
86
+ }
87
+ /** A replay's existing outcome, decided before any resource is retained for it. */
88
+ export type TurnQueueLookup<P> = {
89
+ kind: "duplicate";
90
+ entry: TurnQueueEntry<P>;
91
+ position: number;
92
+ } | {
93
+ kind: "settled";
94
+ outcome: "dispatched" | "cancelled";
95
+ } | {
96
+ kind: "conflict";
97
+ message: string;
98
+ };
99
+ export type TurnQueueEnqueueResult<P> = {
100
+ kind: "queued";
101
+ entry: TurnQueueEntry<P>;
102
+ position: number;
103
+ } | {
104
+ kind: "duplicate";
105
+ entry: TurnQueueEntry<P>;
106
+ position: number;
107
+ } | {
108
+ kind: "settled";
109
+ outcome: "dispatched" | "cancelled";
110
+ } | {
111
+ kind: "rejected";
112
+ message: string;
113
+ };
114
+ export declare function turnQueueTargetKey(target: MobileTurnQueueTarget): string;
115
+ export declare function sameTurnQueueTarget(left: MobileTurnQueueTarget | undefined, right: MobileTurnQueueTarget | undefined): boolean;
116
+ export declare class MobileTurnQueue<P> {
117
+ private readonly deps;
118
+ readonly epoch: `${string}-${string}-${string}-${string}-${string}`;
119
+ private readonly queues;
120
+ private readonly settled;
121
+ private readonly pumping;
122
+ private readonly stopGenerations;
123
+ private readonly now;
124
+ private timer?;
125
+ private disposed;
126
+ /** Monotonic across targets so a recreated queue never reuses a revision. */
127
+ private revision;
128
+ constructor(deps: MobileTurnQueueDeps<P>);
129
+ /**
130
+ * The live or settled outcome of one device's intent on one target. Callers
131
+ * check this BEFORE retaining resources: a replay of an admitted turn must be
132
+ * answered from its receipt, not re-validated against consumed uploads.
133
+ */
134
+ lookup(target: MobileTurnQueueTarget, deviceId: string, clientMessageId: string, fingerprint?: string): TurnQueueLookup<P> | undefined;
135
+ /** Admit one submission, or return its existing/settled outcome for a replay. */
136
+ enqueue(submission: TurnQueueSubmission<P>): TurnQueueEnqueueResult<P>;
137
+ /** Remove one entry that has not been handed to its submit path. */
138
+ cancel(target: MobileTurnQueueTarget, entryId: string): TurnQueueActionResult;
139
+ /** Remove every entry that is not already being submitted. */
140
+ clear(target: MobileTurnQueueTarget): number;
141
+ /**
142
+ * Stop scheduling after the current turn; nothing is discarded. An explicit
143
+ * Stop outranks a failure/uncertain pause: removing or retrying the entry
144
+ * that caused that pause must not restart the rest. Every Stop advances the
145
+ * target's stop generation, which fences an in-flight dispatch off before it
146
+ * physically submits (see stopGeneration).
147
+ */
148
+ pause(target: MobileTurnQueueTarget, reason: "stopped"): void;
149
+ /** Monotonic count of Stops of this target during this Host lifetime. */
150
+ stopGeneration(target: MobileTurnQueueTarget): number;
151
+ /**
152
+ * Continue after Stop or a rejected entry. An entry whose execution cannot
153
+ * be ruled out must be removed first; it is never resubmitted from here.
154
+ */
155
+ resume(target: MobileTurnQueueTarget): TurnQueueActionResult;
156
+ /** Retry a rejected entry with the same clientMessageId. */
157
+ retry(target: MobileTurnQueueTarget, entryId: string): TurnQueueActionResult;
158
+ private releaseFailurePause;
159
+ /** Publish an entry whose submitter was told it is queued rather than running. */
160
+ reveal(target: MobileTurnQueueTarget, entryId: string): void;
161
+ has(target: MobileTurnQueueTarget): boolean;
162
+ entries(target: MobileTurnQueueTarget): readonly Readonly<TurnQueueEntry<P>>[];
163
+ targets(): MobileTurnQueueTarget[];
164
+ /** Public state as seen by one recipient device. */
165
+ snapshot(target: MobileTurnQueueTarget, recipientDeviceId?: string): MobileTurnQueueState;
166
+ /** Re-evaluate every queue; used by the idle timer and Host lifecycle signals. */
167
+ pumpAll(): Promise<void>;
168
+ /**
169
+ * Hand the next runnable entry to its submit path when the exact target is
170
+ * idle. At most one entry per target is in flight. Entries of one device
171
+ * keep their order; a device that cannot run now (e.g. its tab is away)
172
+ * does not hold back other devices' turns. A rejected or uncertain entry
173
+ * pauses the whole target so nothing runs past an unresolved outcome.
174
+ */
175
+ pump(target: MobileTurnQueueTarget): Promise<void>;
176
+ dispose(): void;
177
+ private identity;
178
+ private retire;
179
+ private remember;
180
+ private changed;
181
+ private ensureTimer;
182
+ private stopTimerIfIdle;
183
+ }