@ricsam/r5d-worker 0.0.67 → 0.0.68

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
@@ -38,4 +38,6 @@ worker exec macos docker compose up -d
38
38
  worker exec macos bun test
39
39
  ```
40
40
 
41
+ Agent shell commands receive no stdin by default: prompts read immediate EOF instead of hanging. A command started with `interactive: true` is spawned with a writable stdin pipe, and the agent's `shell_write` tool delivers input through the `exec_stdin` protocol message. The worker acknowledges each write with an `operation_result` carrying the delivered byte count, closes stdin on an explicit `eof`, on cancellation, on timeout, and at process exit, and advertises this support through the `execStdinV1` capability in its hello message. Servers refuse interactive runs and stdin writes for workers without the capability, so this is a protocol-compatible release that uses the ordinary worker update flow. Delivered stdin is audited server-side in the run's `process-runs/<runId>/stdin.log`.
42
+
41
43
  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.
package/dist/cjs/main.cjs CHANGED
@@ -1960,8 +1960,12 @@ async function executeStreamingCommand(input) {
1960
1960
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1961
1961
  });
1962
1962
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
1963
+ const interactive = input.message.interactive === true;
1963
1964
  const subprocess = Bun.spawn(input.message.argv, {
1964
1965
  cwd,
1966
+ // Without an explicit stdin the process reads /dev/null and interactive
1967
+ // prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
1968
+ ...interactive ? { stdin: "pipe" } : {},
1965
1969
  stdout: "pipe",
1966
1970
  stderr: "pipe",
1967
1971
  detached: true,
@@ -1983,7 +1987,8 @@ async function executeStreamingCommand(input) {
1983
1987
  argv: input.message.argv,
1984
1988
  command: input.message.command,
1985
1989
  cwd,
1986
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1990
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1991
+ ...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
1987
1992
  });
1988
1993
  started = true;
1989
1994
  sendWorkerMessage(input.ws, {
@@ -1997,6 +2002,7 @@ async function executeStreamingCommand(input) {
1997
2002
  if (input.message.timeoutMs) {
1998
2003
  timeout = setTimeout(() => {
1999
2004
  timedOut = true;
2005
+ closeProcessStdin(activeProcesses.get(input.message.runId));
2000
2006
  void (0, import_process_tree.terminateProcessTree)(subprocess).catch((error) => {
2001
2007
  process.stderr.write(
2002
2008
  `[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
@@ -2056,10 +2062,21 @@ async function executeStreamingCommand(input) {
2056
2062
  if (timeout) {
2057
2063
  clearTimeout(timeout);
2058
2064
  }
2065
+ closeProcessStdin(activeProcesses.get(input.message.runId));
2059
2066
  activeProcesses.delete(input.message.runId);
2060
2067
  cancelledProcessRuns.delete(input.message.runId);
2061
2068
  }
2062
2069
  }
2070
+ function closeProcessStdin(active) {
2071
+ if (!active?.stdin) {
2072
+ return;
2073
+ }
2074
+ try {
2075
+ void active.stdin.end();
2076
+ } catch {
2077
+ }
2078
+ active.stdin = void 0;
2079
+ }
2063
2080
  function sendWorkerMessage(ws, message) {
2064
2081
  const target = currentWorkerSocket ?? ws;
2065
2082
  try {
@@ -2083,7 +2100,8 @@ function buildActiveProcessReports() {
2083
2100
  argv: active.argv,
2084
2101
  command: active.command,
2085
2102
  ...active.cwd ? { cwd: active.cwd } : {},
2086
- startedAt: active.startedAt
2103
+ startedAt: active.startedAt,
2104
+ ...active.interactive ? { interactive: true } : {}
2087
2105
  }));
2088
2106
  }
2089
2107
  function sendActiveProcessReport(ws) {
@@ -2889,7 +2907,8 @@ async function startWorker(options) {
2889
2907
  browserPortForwarding: true,
2890
2908
  globalWorkspaceSyncLeaseV1: true,
2891
2909
  eventualWorkspaceSyncV1: true,
2892
- workspaceIncidentCandidatePinV1: true
2910
+ workspaceIncidentCandidatePinV1: true,
2911
+ execStdinV1: true
2893
2912
  },
2894
2913
  projectRoot: projectsRoot,
2895
2914
  artifactRoot,
@@ -3332,6 +3351,7 @@ async function startWorker(options) {
3332
3351
  let cancelMessage;
3333
3352
  if (active) {
3334
3353
  try {
3354
+ closeProcessStdin(active);
3335
3355
  await (0, import_process_tree.terminateProcessTree)(active.process);
3336
3356
  cancelMessage = `Stopped ${active.command}`;
3337
3357
  } catch (error) {
@@ -3353,6 +3373,51 @@ async function startWorker(options) {
3353
3373
  );
3354
3374
  return;
3355
3375
  }
3376
+ if (message.type === "exec_stdin") {
3377
+ const sendAck = (payload) => ws.send(
3378
+ JSON.stringify({
3379
+ type: "operation_result",
3380
+ requestId: message.requestId,
3381
+ ...payload
3382
+ })
3383
+ );
3384
+ const active = activeProcesses.get(message.runId);
3385
+ if (!active) {
3386
+ sendAck({ error: `Process run ${message.runId} is not active on this worker` });
3387
+ return;
3388
+ }
3389
+ if (!active.interactive || !active.stdin) {
3390
+ sendAck({
3391
+ error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
3392
+ });
3393
+ return;
3394
+ }
3395
+ try {
3396
+ let bytesWritten = 0;
3397
+ if (message.data !== void 0 && message.data.length > 0) {
3398
+ bytesWritten = await active.stdin.write(message.data);
3399
+ await active.stdin.flush();
3400
+ }
3401
+ if (message.eof === true) {
3402
+ await active.stdin.end();
3403
+ active.stdin = void 0;
3404
+ }
3405
+ if (targetMayMutateVisibleWorkspace(active.target)) {
3406
+ markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
3407
+ }
3408
+ sendAck({
3409
+ result: {
3410
+ type: "exec_stdin",
3411
+ runId: message.runId,
3412
+ bytesWritten,
3413
+ eof: message.eof === true
3414
+ }
3415
+ });
3416
+ } catch (error) {
3417
+ sendAck({ error: error instanceof Error ? error.message : String(error) });
3418
+ }
3419
+ return;
3420
+ }
3356
3421
  if (message.type === "pty_open") {
3357
3422
  if (cliUpdateInProgress) {
3358
3423
  sendWorkerMessage(ws, {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.67",
3
+ "version": "0.0.68",
4
4
  "type": "commonjs"
5
5
  }
package/dist/mjs/main.mjs CHANGED
@@ -1931,8 +1931,12 @@ async function executeStreamingCommand(input) {
1931
1931
  activePlanId: input.message.env?.R5D_ACTIVE_PLAN_ID
1932
1932
  });
1933
1933
  const cwd = resolveCommandCwd(input.resolvedTarget.rootPath, input.message.cwd);
1934
+ const interactive = input.message.interactive === true;
1934
1935
  const subprocess = Bun.spawn(input.message.argv, {
1935
1936
  cwd,
1937
+ // Without an explicit stdin the process reads /dev/null and interactive
1938
+ // prompts see immediate EOF; "pipe" keeps stdin open for exec_stdin.
1939
+ ...interactive ? { stdin: "pipe" } : {},
1936
1940
  stdout: "pipe",
1937
1941
  stderr: "pipe",
1938
1942
  detached: true,
@@ -1954,7 +1958,8 @@ async function executeStreamingCommand(input) {
1954
1958
  argv: input.message.argv,
1955
1959
  command: input.message.command,
1956
1960
  cwd,
1957
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1961
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1962
+ ...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
1958
1963
  });
1959
1964
  started = true;
1960
1965
  sendWorkerMessage(input.ws, {
@@ -1968,6 +1973,7 @@ async function executeStreamingCommand(input) {
1968
1973
  if (input.message.timeoutMs) {
1969
1974
  timeout = setTimeout(() => {
1970
1975
  timedOut = true;
1976
+ closeProcessStdin(activeProcesses.get(input.message.runId));
1971
1977
  void terminateProcessTree(subprocess).catch((error) => {
1972
1978
  process.stderr.write(
1973
1979
  `[r5d-worker] failed to terminate timed-out process ${input.message.runId}: ${error instanceof Error ? error.message : String(error)}
@@ -2027,10 +2033,21 @@ async function executeStreamingCommand(input) {
2027
2033
  if (timeout) {
2028
2034
  clearTimeout(timeout);
2029
2035
  }
2036
+ closeProcessStdin(activeProcesses.get(input.message.runId));
2030
2037
  activeProcesses.delete(input.message.runId);
2031
2038
  cancelledProcessRuns.delete(input.message.runId);
2032
2039
  }
2033
2040
  }
2041
+ function closeProcessStdin(active) {
2042
+ if (!active?.stdin) {
2043
+ return;
2044
+ }
2045
+ try {
2046
+ void active.stdin.end();
2047
+ } catch {
2048
+ }
2049
+ active.stdin = void 0;
2050
+ }
2034
2051
  function sendWorkerMessage(ws, message) {
2035
2052
  const target = currentWorkerSocket ?? ws;
2036
2053
  try {
@@ -2054,7 +2071,8 @@ function buildActiveProcessReports() {
2054
2071
  argv: active.argv,
2055
2072
  command: active.command,
2056
2073
  ...active.cwd ? { cwd: active.cwd } : {},
2057
- startedAt: active.startedAt
2074
+ startedAt: active.startedAt,
2075
+ ...active.interactive ? { interactive: true } : {}
2058
2076
  }));
2059
2077
  }
2060
2078
  function sendActiveProcessReport(ws) {
@@ -2860,7 +2878,8 @@ async function startWorker(options) {
2860
2878
  browserPortForwarding: true,
2861
2879
  globalWorkspaceSyncLeaseV1: true,
2862
2880
  eventualWorkspaceSyncV1: true,
2863
- workspaceIncidentCandidatePinV1: true
2881
+ workspaceIncidentCandidatePinV1: true,
2882
+ execStdinV1: true
2864
2883
  },
2865
2884
  projectRoot: projectsRoot,
2866
2885
  artifactRoot,
@@ -3303,6 +3322,7 @@ async function startWorker(options) {
3303
3322
  let cancelMessage;
3304
3323
  if (active) {
3305
3324
  try {
3325
+ closeProcessStdin(active);
3306
3326
  await terminateProcessTree(active.process);
3307
3327
  cancelMessage = `Stopped ${active.command}`;
3308
3328
  } catch (error) {
@@ -3324,6 +3344,51 @@ async function startWorker(options) {
3324
3344
  );
3325
3345
  return;
3326
3346
  }
3347
+ if (message.type === "exec_stdin") {
3348
+ const sendAck = (payload) => ws.send(
3349
+ JSON.stringify({
3350
+ type: "operation_result",
3351
+ requestId: message.requestId,
3352
+ ...payload
3353
+ })
3354
+ );
3355
+ const active = activeProcesses.get(message.runId);
3356
+ if (!active) {
3357
+ sendAck({ error: `Process run ${message.runId} is not active on this worker` });
3358
+ return;
3359
+ }
3360
+ if (!active.interactive || !active.stdin) {
3361
+ sendAck({
3362
+ error: `Process run ${message.runId} has no open stdin. Start a new shell command with "interactive": true to write to its stdin.`
3363
+ });
3364
+ return;
3365
+ }
3366
+ try {
3367
+ let bytesWritten = 0;
3368
+ if (message.data !== void 0 && message.data.length > 0) {
3369
+ bytesWritten = await active.stdin.write(message.data);
3370
+ await active.stdin.flush();
3371
+ }
3372
+ if (message.eof === true) {
3373
+ await active.stdin.end();
3374
+ active.stdin = void 0;
3375
+ }
3376
+ if (targetMayMutateVisibleWorkspace(active.target)) {
3377
+ markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
3378
+ }
3379
+ sendAck({
3380
+ result: {
3381
+ type: "exec_stdin",
3382
+ runId: message.runId,
3383
+ bytesWritten,
3384
+ eof: message.eof === true
3385
+ }
3386
+ });
3387
+ } catch (error) {
3388
+ sendAck({ error: error instanceof Error ? error.message : String(error) });
3389
+ }
3390
+ return;
3391
+ }
3327
3392
  if (message.type === "pty_open") {
3328
3393
  if (cliUpdateInProgress) {
3329
3394
  sendWorkerMessage(ws, {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.67",
3
+ "version": "0.0.68",
4
4
  "type": "module"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.67",
3
+ "version": "0.0.68",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",