@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -0,0 +1,397 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ fstatSync,
6
+ lstatSync,
7
+ openSync,
8
+ opendirSync,
9
+ readSync,
10
+ realpathSync,
11
+ statSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { basename, dirname, isAbsolute, posix, relative, resolve, sep, win32 } from "node:path";
16
+ import { renameAtomicFile } from "../lib/windows-atomic-replace";
17
+ import {
18
+ REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES,
19
+ type RemoteWorkspaceToolName,
20
+ type RemoteWorkspaceToolResult,
21
+ } from "./workspace-tools";
22
+
23
+ export interface RemoteWorkspaceRoot {
24
+ id: string;
25
+ path: string;
26
+ }
27
+
28
+ export interface RemoteWorkspaceExecutionRequest {
29
+ requestId: string;
30
+ sessionId: string;
31
+ executorDeviceId: string;
32
+ rootId: string;
33
+ tool: RemoteWorkspaceToolName;
34
+ arguments: unknown;
35
+ }
36
+
37
+ export interface RemoteWorkspaceExecutorOptions {
38
+ deviceId: string;
39
+ roots: readonly RemoteWorkspaceRoot[];
40
+ maxOutputBytes?: number;
41
+ platform?: NodeJS.Platform;
42
+ /** Production must provide an OS-sandboxed runner. Omission disables command execution. */
43
+ commandRunner?: RemoteWorkspaceCommandRunner;
44
+ }
45
+
46
+ export interface RemoteWorkspaceCommandRequest {
47
+ command: string[];
48
+ root: string;
49
+ cwd: string;
50
+ timeoutMs: number;
51
+ maxOutputBytes: number;
52
+ signal?: AbortSignal;
53
+ }
54
+
55
+ export interface RemoteWorkspaceCommandResult {
56
+ exitCode: number;
57
+ stdout: string;
58
+ stderr: string;
59
+ }
60
+
61
+ export interface RemoteWorkspaceCommandRunner {
62
+ run(request: RemoteWorkspaceCommandRequest): Promise<RemoteWorkspaceCommandResult>;
63
+ }
64
+
65
+ interface ApprovedRoot {
66
+ id: string;
67
+ path: string;
68
+ dev: number;
69
+ ino: number;
70
+ birthtimeMs: number;
71
+ }
72
+
73
+ function objectArguments(value: unknown): Record<string, unknown> {
74
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
75
+ throw new Error("remote workspace arguments must be an object");
76
+ }
77
+ return value as Record<string, unknown>;
78
+ }
79
+
80
+ function noExtraKeys(value: Record<string, unknown>, allowed: readonly string[]): void {
81
+ const set = new Set(allowed);
82
+ if (Object.keys(value).some(key => !set.has(key))) throw new Error("unknown remote workspace argument");
83
+ }
84
+
85
+ const WINDOWS_RESERVED_BASENAME = /^(?:con|prn|aux|nul|clock\$|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\..*)?$/i;
86
+
87
+ export function validateRemoteWorkspaceRelativePath(
88
+ value: unknown,
89
+ fallback?: string,
90
+ platform: NodeJS.Platform = process.platform,
91
+ ): string {
92
+ const path = value === undefined ? fallback : value;
93
+ if (typeof path !== "string" || path.length < 1 || path.length > 4096 || path.includes("\0")) {
94
+ throw new Error("invalid remote workspace path");
95
+ }
96
+ const paths = platform === "win32" ? win32 : posix;
97
+ if (paths.isAbsolute(path) || /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\")) {
98
+ throw new Error("remote workspace path must be relative");
99
+ }
100
+ if (platform === "win32") {
101
+ for (const segment of path.split(/[\\/]/)) {
102
+ if (!segment || segment === "." || segment === "..") continue;
103
+ if (/[\x01-\x1f<>:"|?*]/.test(segment) || /[ .]$/.test(segment) || WINDOWS_RESERVED_BASENAME.test(segment)) {
104
+ throw new Error("remote workspace path is not a safe Windows file path");
105
+ }
106
+ }
107
+ }
108
+ return path;
109
+ }
110
+
111
+ function inside(root: string, candidate: string): boolean {
112
+ const fromRoot = relative(root, candidate);
113
+ return fromRoot === "" || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot));
114
+ }
115
+
116
+ function errorCode(error: unknown): string | undefined {
117
+ if (!error || typeof error !== "object" || !("code" in error)) return undefined;
118
+ return typeof error.code === "string" ? error.code : undefined;
119
+ }
120
+
121
+ function assertNoSymlinkComponents(root: string, candidate: string, includeLeaf: boolean): void {
122
+ const rel = relative(root, candidate);
123
+ const parts = rel === "" ? [] : rel.split(sep);
124
+ const limit = includeLeaf ? parts.length : Math.max(0, parts.length - 1);
125
+ let current = root;
126
+ for (let index = 0; index < limit; index += 1) {
127
+ current = resolve(current, parts[index]!);
128
+ if (lstatSync(current).isSymbolicLink()) throw new Error("remote workspace symlink traversal is not allowed");
129
+ }
130
+ }
131
+
132
+ function resolveExisting(root: string, value: unknown, platform = process.platform): string {
133
+ const candidate = resolve(root, validateRemoteWorkspaceRelativePath(value, ".", platform));
134
+ if (!inside(root, candidate)) throw new Error("remote workspace path escapes the approved root");
135
+ assertNoSymlinkComponents(root, candidate, true);
136
+ const canonical = realpathSync(candidate);
137
+ if (!inside(root, canonical)) throw new Error("remote workspace path escapes the approved root");
138
+ return canonical;
139
+ }
140
+
141
+ function resolveWritable(root: string, value: unknown, platform = process.platform): string {
142
+ const candidate = resolve(root, validateRemoteWorkspaceRelativePath(value, undefined, platform));
143
+ if (!inside(root, candidate) || candidate === root) throw new Error("remote workspace path escapes the approved root");
144
+ const parent = dirname(candidate);
145
+ assertNoSymlinkComponents(root, parent, true);
146
+ const canonicalParent = realpathSync(parent);
147
+ if (!inside(root, canonicalParent)) throw new Error("remote workspace parent escapes the approved root");
148
+ try {
149
+ assertNoSymlinkComponents(root, candidate, true);
150
+ } catch (error) {
151
+ if (errorCode(error) !== "ENOENT") throw error;
152
+ }
153
+ return resolve(canonicalParent, basename(candidate));
154
+ }
155
+
156
+ function sha256(value: Uint8Array): string {
157
+ return createHash("sha256").update(value).digest("hex");
158
+ }
159
+
160
+ function boundedInteger(value: unknown, fallback: number, minimum: number, maximum: number): number {
161
+ const selected = value === undefined ? fallback : value;
162
+ if (typeof selected !== "number" || !Number.isSafeInteger(selected) || selected < minimum || selected > maximum) {
163
+ throw new Error("invalid remote workspace numeric argument");
164
+ }
165
+ return selected;
166
+ }
167
+
168
+ function decodeUtf8(value: Uint8Array): string {
169
+ return new TextDecoder("utf-8", { fatal: false }).decode(value);
170
+ }
171
+
172
+ function assertOpenedRegularFile(root: string, target: string, descriptor: number, maximum: number) {
173
+ const opened = fstatSync(descriptor);
174
+ const linked = lstatSync(target);
175
+ if (opened.isFile() && linked.isFile() && (opened.nlink !== 1 || linked.nlink !== 1)) {
176
+ throw new Error("remote workspace hard-linked files are not allowed");
177
+ }
178
+ if (!opened.isFile() || !linked.isFile() || linked.isSymbolicLink()
179
+ || opened.dev !== linked.dev || opened.ino !== linked.ino
180
+ || opened.birthtimeMs !== linked.birthtimeMs) {
181
+ throw new Error("remote workspace file identity changed during access");
182
+ }
183
+ const canonical = realpathSync(target);
184
+ if (!inside(root, canonical)) throw new Error("remote workspace path escapes the approved root");
185
+ if (opened.size > maximum) throw new Error("remote workspace file exceeds the read limit");
186
+ return opened;
187
+ }
188
+
189
+ function readBoundedRegularFile(root: string, target: string, maximum: number): { body: Buffer; mode: number } {
190
+ const noFollow = typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
191
+ const nonBlock = typeof constants.O_NONBLOCK === "number" ? constants.O_NONBLOCK : 0;
192
+ const descriptor = openSync(target, constants.O_RDONLY | noFollow | nonBlock);
193
+ try {
194
+ const metadata = assertOpenedRegularFile(root, target, descriptor, maximum);
195
+ const body = Buffer.alloc(metadata.size);
196
+ let offset = 0;
197
+ while (offset < body.byteLength) {
198
+ const read = readSync(descriptor, body, offset, body.byteLength - offset, null);
199
+ if (read === 0) break;
200
+ offset += read;
201
+ }
202
+ assertOpenedRegularFile(root, target, descriptor, maximum);
203
+ return { body: offset === body.byteLength ? body : body.subarray(0, offset), mode: metadata.mode & 0o777 };
204
+ } finally {
205
+ closeSync(descriptor);
206
+ }
207
+ }
208
+
209
+ function assertStableWritableParent(root: string, target: string): void {
210
+ const parent = dirname(target);
211
+ assertNoSymlinkComponents(root, parent, true);
212
+ const canonical = realpathSync(parent);
213
+ if (!inside(root, canonical) || relative(parent, canonical) !== "") {
214
+ throw new Error("remote workspace write parent changed during access");
215
+ }
216
+ }
217
+
218
+ function assertApprovedRootIdentity(root: ApprovedRoot): void {
219
+ const linked = lstatSync(root.path);
220
+ const canonical = realpathSync(root.path);
221
+ if (!linked.isDirectory() || linked.isSymbolicLink()
222
+ || linked.dev !== root.dev || linked.ino !== root.ino
223
+ || linked.birthtimeMs !== root.birthtimeMs
224
+ || relative(root.path, canonical) !== "") {
225
+ throw new Error("remote workspace approved root identity changed; pair the folder again");
226
+ }
227
+ }
228
+
229
+ function assertWritePrecondition(root: string, target: string, expectedSha256: string | null): number {
230
+ try {
231
+ const current = readBoundedRegularFile(root, target, REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES);
232
+ if (expectedSha256 === null || sha256(current.body) !== expectedSha256) {
233
+ throw new Error("remote workspace file changed before write");
234
+ }
235
+ return current.mode;
236
+ } catch (error) {
237
+ if (errorCode(error) !== "ENOENT") throw error;
238
+ if (expectedSha256 !== null) throw new Error("remote workspace file is missing");
239
+ return 0o600;
240
+ }
241
+ }
242
+
243
+ export class RemoteWorkspaceExecutor {
244
+ private readonly roots = new Map<string, ApprovedRoot>();
245
+ private readonly maxOutputBytes: number;
246
+ private operationTail: Promise<void> = Promise.resolve();
247
+
248
+ constructor(private readonly options: RemoteWorkspaceExecutorOptions) {
249
+ if (!options.deviceId || options.deviceId.length > 256) throw new Error("invalid remote workspace executor device ID");
250
+ this.maxOutputBytes = options.maxOutputBytes ?? REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES;
251
+ if (!Number.isSafeInteger(this.maxOutputBytes) || this.maxOutputBytes < 1024) {
252
+ throw new Error("invalid remote workspace output limit");
253
+ }
254
+ for (const root of options.roots) {
255
+ if (!root.id || root.id.length > 128 || this.roots.has(root.id)) throw new Error("invalid remote workspace root ID");
256
+ const metadata = lstatSync(root.path);
257
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("remote workspace root must be a real directory");
258
+ const canonical = realpathSync(root.path);
259
+ const identity = lstatSync(canonical);
260
+ this.roots.set(root.id, {
261
+ id: root.id,
262
+ path: canonical,
263
+ dev: identity.dev,
264
+ ino: identity.ino,
265
+ birthtimeMs: identity.birthtimeMs,
266
+ });
267
+ }
268
+ if (this.roots.size === 0) throw new Error("remote workspace executor needs one approved root");
269
+ }
270
+
271
+ hasApprovedRoot(rootId: string): boolean {
272
+ return this.roots.has(rootId);
273
+ }
274
+
275
+ async invoke(request: RemoteWorkspaceExecutionRequest, signal?: AbortSignal): Promise<RemoteWorkspaceToolResult> {
276
+ if (request.executorDeviceId !== this.options.deviceId) {
277
+ return { ok: false, error: "remote workspace executor identity mismatch" };
278
+ }
279
+ const root = this.roots.get(request.rootId);
280
+ if (!root) return { ok: false, error: "remote workspace root is not approved" };
281
+ if (!request.requestId || !request.sessionId) return { ok: false, error: "invalid remote workspace request identity" };
282
+ const previous = this.operationTail;
283
+ let release!: () => void;
284
+ this.operationTail = new Promise<void>(resolvePromise => { release = resolvePromise; });
285
+ await previous;
286
+ try {
287
+ if (signal?.aborted) throw new Error("remote workspace operation was cancelled");
288
+ assertApprovedRootIdentity(root);
289
+ switch (request.tool) {
290
+ case "list_directory": return { ok: true, value: this.listDirectory(root, request.arguments) };
291
+ case "read_file": return { ok: true, value: this.readFile(root, request.arguments) };
292
+ case "write_file": return { ok: true, value: this.writeFile(root, request.arguments) };
293
+ case "exec": return { ok: true, value: await this.exec(root, request.arguments, signal) };
294
+ }
295
+ } catch (error) {
296
+ return { ok: false, error: error instanceof Error ? error.message : "remote workspace operation failed" };
297
+ } finally {
298
+ release();
299
+ }
300
+ }
301
+
302
+ private listDirectory(root: ApprovedRoot, input: unknown): unknown {
303
+ const args = objectArguments(input);
304
+ noExtraKeys(args, ["path"]);
305
+ const target = resolveExisting(root.path, args.path ?? ".", this.options.platform);
306
+ if (!statSync(target).isDirectory()) throw new Error("remote workspace list target is not a directory");
307
+ const directory = opendirSync(target);
308
+ const entries: Array<{ name: string; type: "directory" | "file" | "symlink" | "other" }> = [];
309
+ try {
310
+ while (true) {
311
+ const entry = directory.readSync();
312
+ if (!entry) break;
313
+ if (entries.length >= 4096) throw new Error("remote workspace directory has too many entries");
314
+ entries.push({
315
+ name: entry.name,
316
+ type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : entry.isSymbolicLink() ? "symlink" : "other",
317
+ });
318
+ }
319
+ } finally {
320
+ directory.closeSync();
321
+ }
322
+ return {
323
+ path: relative(root.path, target) || ".",
324
+ entries,
325
+ };
326
+ }
327
+
328
+ private readFile(root: ApprovedRoot, input: unknown): unknown {
329
+ const args = objectArguments(input);
330
+ noExtraKeys(args, ["path", "maxBytes"]);
331
+ const target = resolveExisting(root.path, args.path, this.options.platform);
332
+ const maxBytes = boundedInteger(args.maxBytes, REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES, 1, this.maxOutputBytes);
333
+ const { body } = readBoundedRegularFile(root.path, target, maxBytes);
334
+ return { path: relative(root.path, target), content: decodeUtf8(body), sha256: sha256(body), bytes: body.byteLength };
335
+ }
336
+
337
+ private writeFile(root: ApprovedRoot, input: unknown): unknown {
338
+ const args = objectArguments(input);
339
+ noExtraKeys(args, ["path", "content", "expectedSha256"]);
340
+ if (typeof args.content !== "string") throw new Error("remote workspace file content must be text");
341
+ const body = Buffer.from(args.content, "utf8");
342
+ if (body.byteLength > REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES) throw new Error("remote workspace file exceeds the write limit");
343
+ const expectedSha256 = args.expectedSha256;
344
+ if (expectedSha256 !== null && (typeof expectedSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedSha256))) {
345
+ throw new Error("invalid remote workspace expected file hash");
346
+ }
347
+ const target = resolveWritable(root.path, args.path, this.options.platform);
348
+ const mode = assertWritePrecondition(root.path, target, expectedSha256);
349
+ const temporary = resolve(dirname(target), `.${randomUUID()}.ocx-remote-write`);
350
+ try {
351
+ writeFileSync(temporary, body, { flag: "wx", mode });
352
+ assertStableWritableParent(root.path, target);
353
+ assertWritePrecondition(root.path, target, expectedSha256);
354
+ renameAtomicFile(temporary, target, undefined, "remote-workspace");
355
+ } finally {
356
+ try { unlinkSync(temporary); } catch { /* committed or already absent */ }
357
+ }
358
+ return { path: relative(root.path, target), sha256: sha256(body), bytes: body.byteLength };
359
+ }
360
+
361
+ private async exec(root: ApprovedRoot, input: unknown, signal?: AbortSignal): Promise<unknown> {
362
+ if (!this.options.commandRunner) {
363
+ throw new Error("remote workspace command runner is disabled until an OS sandbox is configured");
364
+ }
365
+ const args = objectArguments(input);
366
+ noExtraKeys(args, ["command", "cwd", "timeoutMs"]);
367
+ if (!Array.isArray(args.command) || args.command.length < 1 || args.command.length > 64) {
368
+ throw new Error("invalid remote workspace command vector");
369
+ }
370
+ const command: string[] = [];
371
+ for (const value of args.command) {
372
+ if (typeof value !== "string" || value.length < 1 || value.length > 4096 || value.includes("\0")) {
373
+ throw new Error("invalid remote workspace command vector");
374
+ }
375
+ command.push(value);
376
+ }
377
+ if (command.reduce((total, value) => total + value.length, 0) > 16 * 1024) {
378
+ throw new Error("remote workspace command vector is too large");
379
+ }
380
+ const cwd = resolveExisting(root.path, args.cwd ?? ".", this.options.platform);
381
+ if (!statSync(cwd).isDirectory()) throw new Error("remote workspace command cwd is not a directory");
382
+ const timeoutMs = boundedInteger(args.timeoutMs, 30_000, 1, 60_000);
383
+ const result = await this.options.commandRunner.run({
384
+ command,
385
+ root: root.path,
386
+ cwd,
387
+ timeoutMs,
388
+ maxOutputBytes: this.maxOutputBytes,
389
+ signal,
390
+ });
391
+ const outputBytes = Buffer.byteLength(result.stdout, "utf8") + Buffer.byteLength(result.stderr, "utf8");
392
+ if (outputBytes > this.maxOutputBytes) {
393
+ throw new Error("remote workspace command runner exceeded its output contract");
394
+ }
395
+ return { cwd: relative(root.path, cwd) || ".", ...result };
396
+ }
397
+ }