@ricsam/r5d-worker 0.0.125 → 0.0.127

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.
package/README.md CHANGED
@@ -94,3 +94,7 @@ Cancellation is checked again immediately before spawn. A `cancel` carrying `sco
94
94
  When the connected `r5d-browser` requests a port forward, the worker opens each relayed connection only to `127.0.0.1` on the requested worker port. Browser-side and worker-side ports may differ. The worker never opens a public listener, and a disconnected worker leaves the browser's long-lived mapping unavailable until the same worker label reconnects.
95
95
 
96
96
  Control execution requires an initialized launcher that explicitly declares its control capacity (or direct execution without an external launcher), an updated worker, and r5dctl installed on the worker's own PATH. Missing control support is an update error; a control request never falls back to a general shell. Deploy the app and worker support assets, update the CLI/worker, and reconnect before resuming paused work.
97
+
98
+ ## Uploading files to the signed-in browser
99
+
100
+ `browser_upload_files` transfers regular files from this worker to the connected r5d-browser and attaches them to the selected file input in one agent operation. Source paths may be anywhere this worker can read, including outside the project; relative paths resolve from the session working directory. `$R5D_ROOT`, `$R5D_SESSION_ID`, and `~/` are expanded without executing a shell. File reads are bounded to 1 MiB per RPC, and source metadata is checked throughout the transfer. These transient reads and binary responses bypass recovery journals. Uploads interrupted by a disconnect must be retried as a new operation.
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var browser_upload_exports = {};
30
+ __export(browser_upload_exports, {
31
+ readBrowserUploadChunk: () => readBrowserUploadChunk
32
+ });
33
+ module.exports = __toCommonJS(browser_upload_exports);
34
+ var import_promises = __toESM(require("node:fs/promises"), 1);
35
+ var import_node_fs = require("node:fs");
36
+ var import_node_os = __toESM(require("node:os"), 1);
37
+ var import_node_path = __toESM(require("node:path"), 1);
38
+ var import_browser_upload = require("@ricsam/r5d-api/browser-upload");
39
+ async function readBrowserUploadChunk(input, context) {
40
+ if (typeof input.filePath !== "string" || !input.filePath.trim() || input.filePath.includes("\0"))
41
+ throw new Error("Upload requires a file path.");
42
+ if (!Number.isSafeInteger(input.offset) || input.offset < 0) throw new Error("Invalid upload read offset.");
43
+ if (input.offset > 0 && !input.version) throw new Error("Upload continuation requires a file version.");
44
+ const expanded = input.filePath.replace(/\$\{R5D_ROOT\}|\$R5D_ROOT\b/g, () => context.rootDir).replace(/\$\{R5D_SESSION_ID\}|\$R5D_SESSION_ID\b/g, () => context.sessionId).replace(/^~\//, () => `${import_node_os.default.homedir()}/`);
45
+ const filePath = import_node_path.default.resolve(context.cwd, expanded);
46
+ const handle = await import_promises.default.open(filePath, import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_NONBLOCK);
47
+ try {
48
+ const stat = await handle.stat({ bigint: true });
49
+ if (!stat.isFile()) throw new Error(`Upload source is not a regular file: ${input.filePath}`);
50
+ const metadata = (0, import_browser_upload.validateUploadFile)({ filename: import_node_path.default.basename(filePath), size: Number(stat.size) });
51
+ const versionOf = (s) => `${s.dev}:${s.ino}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
52
+ const version = versionOf(stat);
53
+ if (input.version !== void 0 && input.version !== version)
54
+ throw new Error(`Upload source changed while being transferred: ${input.filePath}`);
55
+ if (input.offset > metadata.size) throw new Error("Upload offset is past the end of the file.");
56
+ const bytes = Buffer.alloc(input.metadataOnly === true ? 0 : Math.min(import_browser_upload.BROWSER_UPLOAD_CHUNK_BYTES, metadata.size - input.offset));
57
+ let read = 0;
58
+ while (read < bytes.length) {
59
+ const { bytesRead } = await handle.read(bytes, read, bytes.length - read, input.offset + read);
60
+ if (!bytesRead) throw new Error(`Upload source was truncated: ${input.filePath}`);
61
+ read += bytesRead;
62
+ }
63
+ if (versionOf(await handle.stat({ bigint: true })) !== version)
64
+ throw new Error(`Upload source changed while being read: ${input.filePath}`);
65
+ return { type: "browser_upload_read", ...metadata, version, offset: input.offset, base64: bytes.toString("base64") };
66
+ } finally {
67
+ await handle.close();
68
+ }
69
+ }
70
+ // Annotate the CommonJS export names for ESM import in node:
71
+ 0 && (module.exports = {
72
+ readBrowserUploadChunk
73
+ });
package/dist/cjs/main.cjs CHANGED
@@ -56,6 +56,7 @@ __export(main_exports, {
56
56
  writeWorkerTextFile: () => writeWorkerTextFile
57
57
  });
58
58
  module.exports = __toCommonJS(main_exports);
59
+ var import_browser_upload = require("./browser-upload.cjs");
59
60
  var import_runtime_version = require("./runtime-version.cjs");
60
61
  var import_plan_paths = require("./plan-paths.cjs");
61
62
  var import_plan_parser = require("./plan-parser.cjs");
@@ -2819,6 +2820,13 @@ async function executeOperation(input) {
2819
2820
  return executeViewFileBytesOperation({ ...input, message: input.message });
2820
2821
  case "code_list":
2821
2822
  return executeCodeListOperation({ ...input, message: input.message });
2823
+ case "browser_upload_read":
2824
+ input.assertAdmission();
2825
+ return (0, import_browser_upload.readBrowserUploadChunk)(input.message.input, {
2826
+ cwd: input.resolvedTarget.rootPath,
2827
+ rootDir: input.rootDir,
2828
+ sessionId: input.message.sessionId
2829
+ });
2822
2830
  case "code_read":
2823
2831
  return executeCodeReadOperation({ ...input, message: input.message });
2824
2832
  }
@@ -3006,6 +3014,7 @@ async function executeStreamingCommand(input) {
3006
3014
  let timeout;
3007
3015
  let spawnedProcess;
3008
3016
  let preparationSettled = false;
3017
+ let stagedEnvFiles;
3009
3018
  const settlePreparation = () => {
3010
3019
  if (preparationSettled) return;
3011
3020
  preparationSettled = true;
@@ -3034,7 +3043,13 @@ async function executeStreamingCommand(input) {
3034
3043
  const interactive = input.message.interactive === true;
3035
3044
  const control = input.message.commandClass === "control";
3036
3045
  let argv = input.message.argv;
3037
- let environment = workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2]);
3046
+ if (control && input.message.envFiles?.length) throw new Error("Control commands cannot receive secret files");
3047
+ stagedEnvFiles = stagePtyEnvFiles(input.message.envFiles);
3048
+ let environment = workerChildProcessEnvironment([
3049
+ githubProcessEnv(),
3050
+ resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
3051
+ targetProcessEnv2
3052
+ ]);
3038
3053
  if (control) {
3039
3054
  (0, import_control_command_policy.assertControlCommandArgv)(argv);
3040
3055
  if (interactive) throw new Error("Control commands cannot be interactive");
@@ -3169,6 +3184,7 @@ async function executeStreamingCommand(input) {
3169
3184
  if (spawnedProcess) {
3170
3185
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
3171
3186
  }
3187
+ if (stagedEnvFiles) removePtyEnvFiles(stagedEnvFiles.paths);
3172
3188
  await releaseWorkerCommandLaunch(input.resources);
3173
3189
  activeProcesses.delete(input.message.runId);
3174
3190
  cancelledProcessRuns.delete(input.message.runId);
@@ -3250,6 +3266,10 @@ function sendReplayWorkerMessage(ws, message) {
3250
3266
  }
3251
3267
  function sendWorkerMessage(ws, message) {
3252
3268
  if (launcherFailureInProgress) return;
3269
+ if (message.type === "browser_upload_read_result") {
3270
+ if (currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
3271
+ return;
3272
+ }
3253
3273
  const transportError = "error" in message && typeof message.error === "string" ? message.error : message.type === "workspace_sync_result" && message.result.outcome === "failed" ? message.result.error : void 0;
3254
3274
  if (recoveryJournal && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
3255
3275
  recoveryJournal.unknown(message.requestId);
@@ -6036,8 +6056,10 @@ async function startWorker(options, projectRuntime = {
6036
6056
  capabilities: {
6037
6057
  updateClis: true,
6038
6058
  browserPortForwarding: true,
6059
+ browserFileUploads: true,
6039
6060
  execStdinV1: true,
6040
6061
  ptyEnvFilesV1: true,
6062
+ execEnvFilesV1: true,
6041
6063
  workspaceRemediationAncestorGuardV1: true,
6042
6064
  workspaceIncidentConfigDeferralV1: true,
6043
6065
  workspaceConfigResetToCanonicalV1: true,
@@ -6172,7 +6194,7 @@ async function startWorker(options, projectRuntime = {
6172
6194
  if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
6173
6195
  if (!startupRecoveryComplete && message.type !== "ping") return;
6174
6196
  if (currentWorkerSocket !== ws) return;
6175
- const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
6197
+ const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" && message.type !== "browser_upload_read" ? message.requestId : void 0;
6176
6198
  if (message.type === "cancel") {
6177
6199
  const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
6178
6200
  const cancelled = await cancelProcessRun(message.runId, message.scope);
@@ -7078,10 +7100,10 @@ async function startWorker(options, projectRuntime = {
7078
7100
  });
7079
7101
  return;
7080
7102
  }
7081
- if (message.type === "read_plan" || message.type === "lint_plans" || message.type === "update_task_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
7103
+ if (message.type === "read_plan" || message.type === "lint_plans" || message.type === "update_task_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read" || message.type === "browser_upload_read") {
7082
7104
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
7083
7105
  sendWorkerMessage(ws, {
7084
- type: "operation_result",
7106
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7085
7107
  requestId: message.requestId,
7086
7108
  error: `Workspace operations are deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}; use a canonical remediation shell`
7087
7109
  });
@@ -7109,7 +7131,7 @@ async function startWorker(options, projectRuntime = {
7109
7131
  sendSerializedWorkerMessage(
7110
7132
  ws,
7111
7133
  JSON.stringify({
7112
- type: "operation_result",
7134
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7113
7135
  requestId: message.requestId,
7114
7136
  error: error instanceof Error ? error.message : String(error)
7115
7137
  })
@@ -7137,7 +7159,7 @@ async function startWorker(options, projectRuntime = {
7137
7159
  sendSerializedWorkerMessage(
7138
7160
  ws,
7139
7161
  JSON.stringify({
7140
- type: "operation_result",
7162
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7141
7163
  requestId: message.requestId,
7142
7164
  result
7143
7165
  })
@@ -7155,7 +7177,7 @@ async function startWorker(options, projectRuntime = {
7155
7177
  sendSerializedWorkerMessage(
7156
7178
  ws,
7157
7179
  JSON.stringify({
7158
- type: "operation_result",
7180
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7159
7181
  requestId: message.requestId,
7160
7182
  error: error instanceof Error ? error.message : String(error)
7161
7183
  })
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.125",
3
+ "version": "0.0.127",
4
4
  "type": "commonjs"
5
5
  }
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import {
6
+ BROWSER_UPLOAD_CHUNK_BYTES,
7
+ validateUploadFile
8
+ } from "@ricsam/r5d-api/browser-upload";
9
+ async function readBrowserUploadChunk(input, context) {
10
+ if (typeof input.filePath !== "string" || !input.filePath.trim() || input.filePath.includes("\0"))
11
+ throw new Error("Upload requires a file path.");
12
+ if (!Number.isSafeInteger(input.offset) || input.offset < 0) throw new Error("Invalid upload read offset.");
13
+ if (input.offset > 0 && !input.version) throw new Error("Upload continuation requires a file version.");
14
+ const expanded = input.filePath.replace(/\$\{R5D_ROOT\}|\$R5D_ROOT\b/g, () => context.rootDir).replace(/\$\{R5D_SESSION_ID\}|\$R5D_SESSION_ID\b/g, () => context.sessionId).replace(/^~\//, () => `${os.homedir()}/`);
15
+ const filePath = path.resolve(context.cwd, expanded);
16
+ const handle = await fs.open(filePath, constants.O_RDONLY | constants.O_NONBLOCK);
17
+ try {
18
+ const stat = await handle.stat({ bigint: true });
19
+ if (!stat.isFile()) throw new Error(`Upload source is not a regular file: ${input.filePath}`);
20
+ const metadata = validateUploadFile({ filename: path.basename(filePath), size: Number(stat.size) });
21
+ const versionOf = (s) => `${s.dev}:${s.ino}:${s.size}:${s.mtimeNs}:${s.ctimeNs}`;
22
+ const version = versionOf(stat);
23
+ if (input.version !== void 0 && input.version !== version)
24
+ throw new Error(`Upload source changed while being transferred: ${input.filePath}`);
25
+ if (input.offset > metadata.size) throw new Error("Upload offset is past the end of the file.");
26
+ const bytes = Buffer.alloc(input.metadataOnly === true ? 0 : Math.min(BROWSER_UPLOAD_CHUNK_BYTES, metadata.size - input.offset));
27
+ let read = 0;
28
+ while (read < bytes.length) {
29
+ const { bytesRead } = await handle.read(bytes, read, bytes.length - read, input.offset + read);
30
+ if (!bytesRead) throw new Error(`Upload source was truncated: ${input.filePath}`);
31
+ read += bytesRead;
32
+ }
33
+ if (versionOf(await handle.stat({ bigint: true })) !== version)
34
+ throw new Error(`Upload source changed while being read: ${input.filePath}`);
35
+ return { type: "browser_upload_read", ...metadata, version, offset: input.offset, base64: bytes.toString("base64") };
36
+ } finally {
37
+ await handle.close();
38
+ }
39
+ }
40
+ export {
41
+ readBrowserUploadChunk
42
+ };
package/dist/mjs/main.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import { readBrowserUploadChunk } from "./browser-upload.mjs";
2
3
  import { WORKER_RUNTIME_VERSION } from "./runtime-version.mjs";
3
4
  import { getWorkerPlanPath, getWorkerPlansPath, validatePlanId } from "./plan-paths.mjs";
4
5
  import { createPlanMarkdown, updateTaskCheckbox } from "./plan-parser.mjs";
@@ -2840,6 +2841,13 @@ async function executeOperation(input) {
2840
2841
  return executeViewFileBytesOperation({ ...input, message: input.message });
2841
2842
  case "code_list":
2842
2843
  return executeCodeListOperation({ ...input, message: input.message });
2844
+ case "browser_upload_read":
2845
+ input.assertAdmission();
2846
+ return readBrowserUploadChunk(input.message.input, {
2847
+ cwd: input.resolvedTarget.rootPath,
2848
+ rootDir: input.rootDir,
2849
+ sessionId: input.message.sessionId
2850
+ });
2843
2851
  case "code_read":
2844
2852
  return executeCodeReadOperation({ ...input, message: input.message });
2845
2853
  }
@@ -3027,6 +3035,7 @@ async function executeStreamingCommand(input) {
3027
3035
  let timeout;
3028
3036
  let spawnedProcess;
3029
3037
  let preparationSettled = false;
3038
+ let stagedEnvFiles;
3030
3039
  const settlePreparation = () => {
3031
3040
  if (preparationSettled) return;
3032
3041
  preparationSettled = true;
@@ -3055,7 +3064,13 @@ async function executeStreamingCommand(input) {
3055
3064
  const interactive = input.message.interactive === true;
3056
3065
  const control = input.message.commandClass === "control";
3057
3066
  let argv = input.message.argv;
3058
- let environment = workerChildProcessEnvironment([githubProcessEnv(), input.message.env ?? {}, targetProcessEnv2]);
3067
+ if (control && input.message.envFiles?.length) throw new Error("Control commands cannot receive secret files");
3068
+ stagedEnvFiles = stagePtyEnvFiles(input.message.envFiles);
3069
+ let environment = workerChildProcessEnvironment([
3070
+ githubProcessEnv(),
3071
+ resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
3072
+ targetProcessEnv2
3073
+ ]);
3059
3074
  if (control) {
3060
3075
  assertControlCommandArgv(argv);
3061
3076
  if (interactive) throw new Error("Control commands cannot be interactive");
@@ -3190,6 +3205,7 @@ async function executeStreamingCommand(input) {
3190
3205
  if (spawnedProcess) {
3191
3206
  await reapCompletedCredentialBearingProcessGroup(input.message.runId, spawnedProcess);
3192
3207
  }
3208
+ if (stagedEnvFiles) removePtyEnvFiles(stagedEnvFiles.paths);
3193
3209
  await releaseWorkerCommandLaunch(input.resources);
3194
3210
  activeProcesses.delete(input.message.runId);
3195
3211
  cancelledProcessRuns.delete(input.message.runId);
@@ -3271,6 +3287,10 @@ function sendReplayWorkerMessage(ws, message) {
3271
3287
  }
3272
3288
  function sendWorkerMessage(ws, message) {
3273
3289
  if (launcherFailureInProgress) return;
3290
+ if (message.type === "browser_upload_read_result") {
3291
+ if (currentWorkerSocket === ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
3292
+ return;
3293
+ }
3274
3294
  const transportError = "error" in message && typeof message.error === "string" ? message.error : message.type === "workspace_sync_result" && message.result.outcome === "failed" ? message.result.error : void 0;
3275
3295
  if (recoveryJournal && transportError && isWorkerCommunicationFailure(transportError) && "requestId" in message && typeof message.requestId === "string") {
3276
3296
  recoveryJournal.unknown(message.requestId);
@@ -6057,8 +6077,10 @@ async function startWorker(options, projectRuntime = {
6057
6077
  capabilities: {
6058
6078
  updateClis: true,
6059
6079
  browserPortForwarding: true,
6080
+ browserFileUploads: true,
6060
6081
  execStdinV1: true,
6061
6082
  ptyEnvFilesV1: true,
6083
+ execEnvFilesV1: true,
6062
6084
  workspaceRemediationAncestorGuardV1: true,
6063
6085
  workspaceIncidentConfigDeferralV1: true,
6064
6086
  workspaceConfigResetToCanonicalV1: true,
@@ -6193,7 +6215,7 @@ async function startWorker(options, projectRuntime = {
6193
6215
  if (!startupRecoveryComplete && message.type !== "ping") await startupRecoveryPromise;
6194
6216
  if (!startupRecoveryComplete && message.type !== "ping") return;
6195
6217
  if (currentWorkerSocket !== ws) return;
6196
- const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" ? message.requestId : void 0;
6218
+ const operationRequestId = "requestId" in message && typeof message.requestId === "string" && message.type !== "workspace_config" && message.type !== "browser_upload_read" ? message.requestId : void 0;
6197
6219
  if (message.type === "cancel") {
6198
6220
  const admission = recoveryJournal.admit({ ...message, requestId: message.requestId });
6199
6221
  const cancelled = await cancelProcessRun(message.runId, message.scope);
@@ -7099,10 +7121,10 @@ async function startWorker(options, projectRuntime = {
7099
7121
  });
7100
7122
  return;
7101
7123
  }
7102
- if (message.type === "read_plan" || message.type === "lint_plans" || message.type === "update_task_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read") {
7124
+ if (message.type === "read_plan" || message.type === "lint_plans" || message.type === "update_task_status" || message.type === "read" || message.type === "write" || message.type === "edit" || message.type === "grep" || message.type === "find" || message.type === "ls" || message.type === "view_file_bytes" || message.type === "code_list" || message.type === "code_read" || message.type === "browser_upload_read") {
7103
7125
  if (workspaceOperationsAreFenced(deferredWorkspaceConfiguration, activeWorkspaceIncidentId)) {
7104
7126
  sendWorkerMessage(ws, {
7105
- type: "operation_result",
7127
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7106
7128
  requestId: message.requestId,
7107
7129
  error: `Workspace operations are deferred for incident ${deferredWorkspaceConfiguration?.incidentId ?? activeWorkspaceIncidentId}; use a canonical remediation shell`
7108
7130
  });
@@ -7130,7 +7152,7 @@ async function startWorker(options, projectRuntime = {
7130
7152
  sendSerializedWorkerMessage(
7131
7153
  ws,
7132
7154
  JSON.stringify({
7133
- type: "operation_result",
7155
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7134
7156
  requestId: message.requestId,
7135
7157
  error: error instanceof Error ? error.message : String(error)
7136
7158
  })
@@ -7158,7 +7180,7 @@ async function startWorker(options, projectRuntime = {
7158
7180
  sendSerializedWorkerMessage(
7159
7181
  ws,
7160
7182
  JSON.stringify({
7161
- type: "operation_result",
7183
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7162
7184
  requestId: message.requestId,
7163
7185
  result
7164
7186
  })
@@ -7176,7 +7198,7 @@ async function startWorker(options, projectRuntime = {
7176
7198
  sendSerializedWorkerMessage(
7177
7199
  ws,
7178
7200
  JSON.stringify({
7179
- type: "operation_result",
7201
+ type: message.type === "browser_upload_read" ? "browser_upload_read_result" : "operation_result",
7180
7202
  requestId: message.requestId,
7181
7203
  error: error instanceof Error ? error.message : String(error)
7182
7204
  })
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.125",
3
+ "version": "0.0.127",
4
4
  "type": "module"
5
5
  }
@@ -0,0 +1,7 @@
1
+ import { type BrowserUploadChunk, type BrowserUploadReadInput } from "@ricsam/r5d-api/browser-upload";
2
+ /** Unlike code-view paths, uploads may read any regular file accessible to this worker. */
3
+ export declare function readBrowserUploadChunk(input: BrowserUploadReadInput, context: {
4
+ cwd: string;
5
+ rootDir: string;
6
+ sessionId: string;
7
+ }): Promise<BrowserUploadChunk>;
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env bun
2
+ import type { BrowserUploadChunk, BrowserUploadReadInput } from "@ricsam/r5d-api/browser-upload";
2
3
  import type { WorkerLintPlansResult, WorkerUpdatePlanTaskResult } from "./plan-paths";
3
4
  import { WORKER_RESUMABLE_PROTOCOL, type WorkerRecoveryClientMessage, type WorkerRecoveryServerMessage } from "./recovery-protocol";
4
5
  import { Database } from "bun:sqlite";
@@ -204,6 +205,11 @@ type WorkerClientMessage = WorkerRecoveryClientMessage | {
204
205
  ptyId: string;
205
206
  requestId?: string;
206
207
  error: string;
208
+ } | {
209
+ type: "browser_upload_read_result";
210
+ requestId: string;
211
+ result?: WorkerOperationResult;
212
+ error?: string;
207
213
  } | {
208
214
  type: "operation_result";
209
215
  requestId: string;
@@ -355,6 +361,12 @@ type WorkerServerMessage = WorkerRecoveryServerMessage | {
355
361
  type: "project";
356
362
  }>;
357
363
  path: string;
364
+ } | {
365
+ type: "browser_upload_read";
366
+ requestId: string;
367
+ target: WorkerSessionTarget;
368
+ sessionId: string;
369
+ input: BrowserUploadReadInput;
358
370
  } | {
359
371
  type: "code_read";
360
372
  requestId: string;
@@ -409,6 +421,12 @@ type WorkerServerMessage = WorkerRecoveryServerMessage | {
409
421
  credentialId?: string;
410
422
  cwd?: string;
411
423
  env?: Record<string, string>;
424
+ envFiles?: Array<{
425
+ path: string;
426
+ content: string;
427
+ mode?: number;
428
+ }>;
429
+ accountEnvironmentWorkerId?: string;
412
430
  timeoutMs?: number;
413
431
  interactive?: boolean;
414
432
  workspaceEffect?: "none";
@@ -639,9 +657,9 @@ type WorkerCodeReadResult = {
639
657
  mtime: string;
640
658
  base64: string;
641
659
  };
642
- type WorkerOperationResult = WorkerReadFileResult | WorkerReadPlanResult | WorkerLintPlansResult | WorkerUpdatePlanTaskResult | WorkerWriteFileResult | WorkerEditFileResult | WorkerGrepResult | WorkerFindResult | WorkerLsResult | WorkerViewFileBytesResult | WorkerExecStdinAck | WorkerCreateProjectBranchResult | WorkerDeleteProjectBranchResult | WorkerCodeListResult | WorkerCodeReadResult;
660
+ type WorkerOperationResult = WorkerReadFileResult | WorkerReadPlanResult | WorkerLintPlansResult | WorkerUpdatePlanTaskResult | WorkerWriteFileResult | WorkerEditFileResult | WorkerGrepResult | WorkerFindResult | WorkerLsResult | WorkerViewFileBytesResult | WorkerExecStdinAck | WorkerCreateProjectBranchResult | WorkerDeleteProjectBranchResult | WorkerCodeListResult | WorkerCodeReadResult | BrowserUploadChunk;
643
661
  type WorkerOperationServerMessage = Extract<WorkerServerMessage, {
644
- type: "read_plan" | "lint_plans" | "update_task_status" | "read" | "write" | "edit" | "grep" | "find" | "ls" | "view_file_bytes" | "code_list" | "code_read";
662
+ type: "read_plan" | "lint_plans" | "update_task_status" | "read" | "write" | "edit" | "grep" | "find" | "ls" | "view_file_bytes" | "code_list" | "code_read" | "browser_upload_read";
645
663
  }>;
646
664
  declare class WorkspaceSyncAdmissionChangedError extends Error {
647
665
  readonly name = "WorkspaceSyncAdmissionChangedError";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.125",
3
+ "version": "0.0.127",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",
@@ -21,6 +21,7 @@
21
21
  "r5d-worker": "dist/cjs/main.cjs"
22
22
  },
23
23
  "dependencies": {
24
+ "@ricsam/r5d-api": "^0.0.127",
24
25
  "node-pty": "^1.1.0",
25
26
  "picomatch": "^4.0.3"
26
27
  },