@runuai/host 0.9.69 → 0.9.70

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.
@@ -35,13 +35,23 @@ type Runner = (args: string[]) => Promise<DockerResult>;
35
35
  const defaultRunner: Runner = (args) => dockerCli(args, { timeoutMs: 60_000 });
36
36
 
37
37
  function machineAbsent(id: string, res: DockerResult): boolean {
38
+ // Daemons vary the casing of the not-found line — Docker Desktop answers
39
+ // "Error: No such object:", OrbStack "error: no such object:" (caught
40
+ // live in the first smoke run: the machine WAS gone and the proof
41
+ // refused it). The prefix is matched case-insensitively; the id itself
42
+ // stays exact — a not-found for "<id>-shadow" still proves nothing.
43
+ // OrbStack also answers with "[]" on stdout beside the error (Docker
44
+ // Desktop prints nothing) — an empty JSON array carries no confusable
45
+ // record, so both are admissible; anything else on stdout is not.
46
+ const stdout = res.stdout.trim();
38
47
  return (
39
48
  typeof res.status === "number" &&
40
49
  res.status !== 0 &&
41
- res.stdout.trim() === "" &&
42
- res.stderr
43
- .split("\n")
44
- .some((line) => line.trim() === `Error: No such object: ${id}`)
50
+ (stdout === "" || stdout === "[]") &&
51
+ res.stderr.split("\n").some((line) => {
52
+ const match = /^error: no such object: (.+)$/i.exec(line.trim());
53
+ return match !== null && match[1] === id;
54
+ })
45
55
  );
46
56
  }
47
57
 
@@ -66,6 +76,7 @@ interface InspectShape {
66
76
  taskLabel: string | null;
67
77
  state: MachineState;
68
78
  address: string | null;
79
+ sshPort: number | null;
69
80
  }
70
81
 
71
82
  /** Strict single-record parse. Exactly one record whose Name is the
@@ -94,12 +105,19 @@ function parseInspect(id: string, stdout: string): InspectShape | null {
94
105
  if (value !== undefined && typeof value !== "string") return null;
95
106
  taskLabel = typeof value === "string" ? value : null;
96
107
  }
97
- const networks = (
108
+ const settings = (
98
109
  record as {
99
- NetworkSettings?: { Networks?: Record<string, { IPAddress?: unknown }> };
110
+ NetworkSettings?: {
111
+ Networks?: Record<string, { IPAddress?: unknown }>;
112
+ Ports?: Record<
113
+ string,
114
+ Array<{ HostIp?: unknown; HostPort?: unknown }> | null
115
+ >;
116
+ };
100
117
  }
101
- ).NetworkSettings?.Networks;
118
+ ).NetworkSettings;
102
119
  let address: string | null = null;
120
+ const networks = settings?.Networks;
103
121
  if (networks && typeof networks === "object") {
104
122
  for (const net of Object.values(networks)) {
105
123
  const ip = net?.IPAddress;
@@ -109,11 +127,29 @@ function parseInspect(id: string, stdout: string): InspectShape | null {
109
127
  }
110
128
  }
111
129
  }
130
+ // A published SSH port (macOS mode) supersedes the bridge IP: the bridge
131
+ // address exists but is unreachable from the Mac, and half-truthful
132
+ // reachability info is worse than none.
133
+ let sshPort: number | null = null;
134
+ const published = settings?.Ports?.["22/tcp"];
135
+ if (Array.isArray(published)) {
136
+ for (const binding of published) {
137
+ const port = Number(binding?.HostPort);
138
+ if (Number.isInteger(port) && port > 0) {
139
+ address = typeof binding?.HostIp === "string" && binding.HostIp.length > 0
140
+ ? binding.HostIp
141
+ : "127.0.0.1";
142
+ sshPort = port;
143
+ break;
144
+ }
145
+ }
146
+ }
112
147
  return {
113
148
  name,
114
149
  taskLabel,
115
150
  state: stateFromDocker(stateObj?.Status),
116
151
  address,
152
+ sshPort,
117
153
  };
118
154
  }
119
155
 
@@ -136,6 +172,9 @@ export function createLocalMachineProvider(
136
172
  taskLabel: record.taskLabel,
137
173
  state: record.state,
138
174
  address: record.state === "running" ? record.address : null,
175
+ ...(record.state === "running" && record.sshPort !== null
176
+ ? { sshPort: record.sshPort }
177
+ : {}),
139
178
  };
140
179
  }
141
180
  if (machineAbsent(id, res)) {
@@ -163,6 +202,15 @@ export function createLocalMachineProvider(
163
202
  String(spec.cpus),
164
203
  "--memory",
165
204
  `${spec.memoryMiB}m`,
205
+ // The image's entrypoint installs this for the ssh user before sshd
206
+ // starts — the local analog of cloud user-data key delivery.
207
+ ...(spec.authorizedPublicKey
208
+ ? ["-e", `UAI_AUTHORIZED_KEY=${spec.authorizedPublicKey}`]
209
+ : []),
210
+ // macOS: bridge IPs are not host-routable; SSH rides a
211
+ // loopback-published ephemeral port instead (never exposed beyond
212
+ // 127.0.0.1).
213
+ ...(spec.publishSsh ? ["-p", "127.0.0.1:0:22"] : []),
166
214
  spec.image,
167
215
  ...(spec.command ?? []),
168
216
  ];
@@ -275,6 +323,9 @@ export function createLocalMachineProvider(
275
323
  taskLabel: record.taskLabel,
276
324
  state: record.state,
277
325
  address: record.state === "running" ? record.address : null,
326
+ ...(record.state === "running" && record.sshPort !== null
327
+ ? { sshPort: record.sshPort }
328
+ : {}),
278
329
  });
279
330
  }
280
331
  return machines;
@@ -35,6 +35,14 @@ export interface MachineSpec {
35
35
  /** Optional long-running entrypoint override (the local backend's images
36
36
  * must keep the machine alive themselves — typically sshd). */
37
37
  command?: string[];
38
+ /** Public key the machine authorizes for the orchestrator's SSH access.
39
+ * Cloud backends deliver it via instance user-data; the local backend via
40
+ * an env var its image's entrypoint installs. */
41
+ authorizedPublicKey?: string;
42
+ /** Local backend on macOS only: container bridge IPs are not
43
+ * host-routable there, so SSH must ride a loopback-published port. The
44
+ * Linux guinea pig and cloud backends dial the address directly. */
45
+ publishSsh?: boolean;
38
46
  }
39
47
 
40
48
  export interface MachineInfo {
@@ -50,6 +58,8 @@ export interface MachineInfo {
50
58
  * reachable; container bridge IPs are NOT host-routable from macOS —
51
59
  * the local backend is for the Linux guinea pig, not Mac hosts. */
52
60
  address: string | null;
61
+ /** SSH port when it differs from 22 (local backend with `publishSsh`). */
62
+ sshPort?: number;
53
63
  /** Present only alongside `unknown` — what the backend actually said. */
54
64
  detail?: string;
55
65
  }
@@ -22,6 +22,10 @@ import { lstatSync } from "node:fs";
22
22
  import path from "node:path";
23
23
 
24
24
  import type { TaskDownResult } from "../agent";
25
+ import {
26
+ hostWorkspaceRead,
27
+ hostWorkspaceWrite,
28
+ } from "./workspace-files";
25
29
  import { assertSafeHostTaskId } from "../task-identity";
26
30
  import {
27
31
  capturedCliExec,
@@ -272,6 +276,23 @@ class AppleContainerTaskEnvironmentHandle
272
276
  };
273
277
  }
274
278
 
279
+ async readWorkspaceFile(filePath: string): Promise<Uint8Array | null> {
280
+ return hostWorkspaceRead(
281
+ this.descriptor,
282
+ path.join(this.#value.hostWorktreePath, "workspace"),
283
+ filePath,
284
+ );
285
+ }
286
+
287
+ async writeWorkspaceFile(filePath: string, bytes: Uint8Array): Promise<void> {
288
+ hostWorkspaceWrite(
289
+ this.descriptor,
290
+ path.join(this.#value.hostWorktreePath, "workspace"),
291
+ filePath,
292
+ bytes,
293
+ );
294
+ }
295
+
275
296
  exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
276
297
  assertTaskEnvironmentProcessRequest(request);
277
298
  return capturedCliExec(
@@ -15,6 +15,10 @@ import {
15
15
  type TaskUpResult,
16
16
  } from "../agent";
17
17
  import { dockerCli, type DockerResult } from "../docker-exec";
18
+ import {
19
+ hostWorkspaceRead,
20
+ hostWorkspaceWrite,
21
+ } from "./workspace-files";
18
22
  import { taskDir } from "../env";
19
23
  import { PREVIEW_SIDECAR_TASK_LABEL } from "../preview-sidecar";
20
24
  import { assertSafeHostTaskId } from "../task-identity";
@@ -271,6 +275,23 @@ class DockerTaskEnvironmentHandle
271
275
  };
272
276
  }
273
277
 
278
+ async readWorkspaceFile(filePath: string): Promise<Uint8Array | null> {
279
+ return hostWorkspaceRead(
280
+ this.descriptor,
281
+ path.join(this.#value.hostWorktreePath, "workspace"),
282
+ filePath,
283
+ );
284
+ }
285
+
286
+ async writeWorkspaceFile(filePath: string, bytes: Uint8Array): Promise<void> {
287
+ hostWorkspaceWrite(
288
+ this.descriptor,
289
+ path.join(this.#value.hostWorktreePath, "workspace"),
290
+ filePath,
291
+ bytes,
292
+ );
293
+ }
294
+
274
295
  exec(request: TaskEnvironmentExecRequest): Promise<TaskEnvironmentExecResult> {
275
296
  assertTaskEnvironmentProcessRequest(request);
276
297
  return capturedCliExec(