@miosa/sdk 3.2.4 → 3.3.0

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/dist/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import { createHmac, timingSafeEqual, randomUUID } from 'crypto';
2
2
  import EventEmitter from 'events';
3
+ import { isIP } from 'net';
4
+ import { resolve4 } from 'dns/promises';
5
+ import * as http2 from 'http2';
6
+ import { createRequire } from 'module';
3
7
 
4
8
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
9
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -18,7 +22,15 @@ var MiosaError = class _MiosaError extends Error {
18
22
  details;
19
23
  requestId;
20
24
  retryable;
21
- constructor(message, status, code, details, requestId, retryable = defaultRetryable(status)) {
25
+ /**
26
+ * Server-supplied retry hint in milliseconds, taken from the canonical
27
+ * error shape's `retry_after_ms` (nested under `error` or top-level) or,
28
+ * failing that, the HTTP `Retry-After` header. `undefined` when the
29
+ * response carried neither, in which case a caller should fall back to
30
+ * its own backoff policy.
31
+ */
32
+ retryAfterMs;
33
+ constructor(message, status, code, details, requestId, retryable = defaultRetryable(status), retryAfterMs) {
22
34
  super(message);
23
35
  this.name = "MiosaError";
24
36
  this.status = status;
@@ -26,34 +38,33 @@ var MiosaError = class _MiosaError extends Error {
26
38
  this.details = details;
27
39
  this.requestId = requestId;
28
40
  this.retryable = retryable;
41
+ this.retryAfterMs = retryAfterMs;
29
42
  }
30
- static fromResponse(status, body5, requestId) {
43
+ /**
44
+ * @param retryAfterHeaderMs The `Retry-After` header value, already
45
+ * converted to milliseconds by the caller. Used only when the body does
46
+ * not carry the more precise `retry_after_ms`.
47
+ */
48
+ static fromResponse(status, body5, requestId, retryAfterHeaderMs2) {
31
49
  const nested = typeof body5.error === "object" ? body5.error : void 0;
32
50
  const flatError = typeof body5.error === "string" ? body5.error : void 0;
33
51
  const flatErrorIsCode = !!flatError && /^[A-Z][A-Z0-9_]+$/.test(flatError);
34
52
  const message = nested?.message ?? body5.message ?? body5.detail ?? body5.reason ?? flatError ?? `HTTP ${status}`;
35
53
  const code = nested?.code ?? body5.code ?? (flatErrorIsCode ? flatError : "UNKNOWN_ERROR");
36
54
  const details = nested?.details ?? body5.details ?? (body5.detail || body5.reason ? { detail: body5.detail, reason: body5.reason } : void 0);
37
- requestId ??= body5.request_id;
55
+ requestId ??= nested?.request_id ?? body5.request_id;
38
56
  const retryable = nested?.retryable ?? body5.retryable ?? defaultRetryable(status);
57
+ const retryAfterMs = nested?.retry_after_ms ?? body5.retry_after_ms ?? retryAfterHeaderMs2;
39
58
  const connectError = connectErrorFromCode(code, message, status, details, requestId, retryable);
40
- if (connectError) return connectError;
41
- if (status === 401 || status === 403) {
42
- return new AuthError(message, status, code, details, requestId, retryable);
43
- }
44
- if (status === 404) {
45
- return new NotFoundError(message, code, details, requestId, retryable);
46
- }
47
- if (status === 429) {
48
- return new RateLimitError(message, details, requestId, void 0, retryable);
49
- }
50
- if (status === 402) {
51
- return new InsufficientCreditsError(message, details, requestId, retryable);
52
- }
53
- if (status >= 400 && status < 500) {
54
- return new ValidationError(message, status, code, details, requestId, retryable);
55
- }
56
- return new _MiosaError(message, status, code, details, requestId, retryable);
59
+ const built = connectError ?? (status === 401 || status === 403 ? new AuthError(message, status, code, details, requestId, retryable) : status === 404 ? new NotFoundError(message, code, details, requestId, retryable) : status === 429 ? new RateLimitError(
60
+ message,
61
+ details,
62
+ requestId,
63
+ retryAfterMs === void 0 ? void 0 : retryAfterMs / 1e3,
64
+ retryable
65
+ ) : status === 402 ? new InsufficientCreditsError(message, details, requestId, retryable) : status >= 400 && status < 500 ? new ValidationError(message, status, code, details, requestId, retryable) : new _MiosaError(message, status, code, details, requestId, retryable));
66
+ built.retryAfterMs = retryAfterMs;
67
+ return built;
57
68
  }
58
69
  };
59
70
  function connectErrorFromCode(code, message, status, details, requestId, retryable) {
@@ -174,7 +185,7 @@ var TokenRefreshFailedError = class extends MiosaError {
174
185
  };
175
186
 
176
187
  // src/version.ts
177
- var SDK_VERSION = "3.2.4";
188
+ var SDK_VERSION = "3.3.0";
178
189
  var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
179
190
 
180
191
  // src/http.ts
@@ -206,15 +217,18 @@ async function ensureHttp2Agent() {
206
217
  ensureHttp2Agent();
207
218
  var DEFAULT_TIMEOUT = 3e4;
208
219
  var DEFAULT_MAX_RETRIES = 3;
209
- var RETRY_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
210
220
  var BASE_DELAY_MS = 500;
211
221
  function sleep(ms) {
212
222
  return new Promise((resolve) => setTimeout(resolve, ms));
213
223
  }
214
- function retryDelay(attempt, retryAfterHeader) {
215
- if (retryAfterHeader !== null) {
216
- const seconds = parseInt(retryAfterHeader, 10);
217
- if (!isNaN(seconds)) return seconds * 1e3;
224
+ function retryAfterHeaderMs(retryAfterHeader) {
225
+ if (retryAfterHeader === null) return void 0;
226
+ const seconds = parseInt(retryAfterHeader, 10);
227
+ return isNaN(seconds) ? void 0 : seconds * 1e3;
228
+ }
229
+ function retryDelay(attempt, hintMs) {
230
+ if (hintMs !== void 0 && Number.isFinite(hintMs) && hintMs >= 0) {
231
+ return hintMs;
218
232
  }
219
233
  return BASE_DELAY_MS * 2 ** attempt + Math.random() * 100;
220
234
  }
@@ -289,7 +303,9 @@ var HttpClient = class {
289
303
  clearTimeout(timer);
290
304
  if (!response.ok) {
291
305
  const requestId = response.headers.get("x-request-id") ?? void 0;
292
- const retryAfter = response.headers.get("retry-after");
306
+ const retryAfterMs = retryAfterHeaderMs(
307
+ response.headers.get("retry-after")
308
+ );
293
309
  let errorBody = {};
294
310
  try {
295
311
  errorBody = await response.json();
@@ -298,11 +314,12 @@ var HttpClient = class {
298
314
  const error = MiosaError.fromResponse(
299
315
  response.status,
300
316
  errorBody,
301
- requestId
317
+ requestId,
318
+ retryAfterMs
302
319
  );
303
- if (RETRY_STATUS.has(response.status) && attempt < maxRetries) {
320
+ if (error.retryable && attempt < maxRetries) {
304
321
  lastError = error;
305
- await sleep(retryDelay(attempt, retryAfter));
322
+ await sleep(retryDelay(attempt, error.retryAfterMs));
306
323
  continue;
307
324
  }
308
325
  throw error;
@@ -333,7 +350,7 @@ var HttpClient = class {
333
350
  );
334
351
  if (attempt < maxRetries) {
335
352
  lastError = networkErr;
336
- await sleep(retryDelay(attempt, null));
353
+ await sleep(retryDelay(attempt, void 0));
337
354
  continue;
338
355
  }
339
356
  throw networkErr;
@@ -437,7 +454,8 @@ var HttpClient = class {
437
454
  throw MiosaError.fromResponse(
438
455
  response.status,
439
456
  errorBody,
440
- response.headers.get("x-request-id") ?? void 0
457
+ response.headers.get("x-request-id") ?? void 0,
458
+ retryAfterHeaderMs(response.headers.get("retry-after"))
441
459
  );
442
460
  }
443
461
  if (!response.body) {
@@ -8652,16 +8670,29 @@ function raiseUnresolvableShape(cpuCount, memoryMb, tier) {
8652
8670
  `Unsupported cpu/memory combination (${supplied}). Supported tiers: xs (1/2048), small (2/4096), medium (4/8192), large (8/16384), xl (16/32768). Pass a matching cpuCount + memoryMb (any diskSizeMb is allowed), use size, or supply cpuCount, memoryMb, and diskSizeMb together for a custom shape.`
8653
8671
  );
8654
8672
  }
8655
- function execDeadline(options) {
8673
+ function validatedExecSeconds(options) {
8656
8674
  const seconds = options?.timeout ?? options?.timeoutSec ?? options?.timeout_sec;
8657
- if (seconds === void 0) return {};
8675
+ if (seconds === void 0) return void 0;
8658
8676
  if (!Number.isFinite(seconds) || seconds <= 0 || seconds > 86400) {
8659
8677
  throw new RangeError(
8660
8678
  "Exec timeout must be greater than zero and at most 86400 seconds"
8661
8679
  );
8662
8680
  }
8681
+ return seconds;
8682
+ }
8683
+ function execDeadline(options) {
8684
+ const seconds = validatedExecSeconds(options);
8685
+ if (seconds === void 0) return {};
8663
8686
  return { timeout: seconds * 1e3 + 5e3 };
8664
8687
  }
8688
+ var DEFAULT_EXEC_WAKE_BUDGET_SEC = 60;
8689
+ function execStreamDeadline(options) {
8690
+ const seconds = validatedExecSeconds(options);
8691
+ if (seconds === void 0) return {};
8692
+ const wakeSeconds = options?.wakeTimeoutSec ?? options?.wake_timeout_sec;
8693
+ const wakeBudgetSec = wakeSeconds !== void 0 ? Math.max(0, wakeSeconds) : DEFAULT_EXEC_WAKE_BUDGET_SEC;
8694
+ return { timeout: (seconds + wakeBudgetSec) * 1e3 + 5e3 };
8695
+ }
8665
8696
  function execBody(command, options = {}) {
8666
8697
  return stripUndefined26({
8667
8698
  command,
@@ -8677,7 +8708,14 @@ function normalizeExecEvent(event, payload) {
8677
8708
  const code = Number(
8678
8709
  record.exit_code ?? record.exitCode ?? (typeof payload === "number" ? payload : 0)
8679
8710
  );
8680
- return { type: "exit", exit_code: code, exitCode: code };
8711
+ const timedOutRaw = record.timed_out ?? record.timedOut;
8712
+ const timedOut = typeof timedOutRaw === "boolean" ? timedOutRaw : void 0;
8713
+ return {
8714
+ type: "exit",
8715
+ exit_code: code,
8716
+ exitCode: code,
8717
+ ...timedOut !== void 0 ? { timed_out: timedOut, timedOut } : {}
8718
+ };
8681
8719
  }
8682
8720
  const type = event === "stderr" ? "stderr" : "stdout";
8683
8721
  const data = typeof payload === "string" ? payload : String(record.line ?? record.data ?? "");
@@ -8744,7 +8782,16 @@ var SandboxFiles = class {
8744
8782
  }
8745
8783
  return response;
8746
8784
  }
8747
- /** POST /api/v1/sandboxes/{id}/files/write-many — write multiple files atomically. */
8785
+ /**
8786
+ * Write a failure-atomic batch on a batch-capable guest.
8787
+ * The guest's guarantee is whatever the response advertises: rely on it only
8788
+ * after `atomicity === "failure_atomic"` and `read_isolation ===
8789
+ * "file_api_operations"` compare equal, because the fields are forwarded
8790
+ * verbatim and an older guest omits them. Never treat an unknown value or a
8791
+ * missing field as the guarantee, and never substitute sequential writes.
8792
+ * Shell/Docker readers may see intermediate publication; coordinate them
8793
+ * with the write if a consistent multi-file view is required.
8794
+ */
8748
8795
  async writeMany(files) {
8749
8796
  const http = this.sandbox.http;
8750
8797
  const payload = files.map((f) => ({
@@ -8819,12 +8866,54 @@ var SandboxSnapshots = class {
8819
8866
  return this.sandbox.deleteSnapshot(snapshotId);
8820
8867
  }
8821
8868
  };
8869
+ var MAX_TERMINAL_ARGS = 256;
8870
+ function invalidTerminalOptions(message, details) {
8871
+ return new MiosaError(
8872
+ message,
8873
+ 400,
8874
+ "INVALID_TERMINAL_OPTIONS",
8875
+ details,
8876
+ void 0,
8877
+ false
8878
+ );
8879
+ }
8880
+ function assertTerminalOptions(params) {
8881
+ if (params.shell !== void 0 && params.command !== void 0) {
8882
+ throw invalidTerminalOptions(
8883
+ "Terminal options cannot combine shell with command: shell names the session shell executable, command starts an executable directly",
8884
+ { shell: params.shell, command: params.command }
8885
+ );
8886
+ }
8887
+ if (params.args === void 0) return;
8888
+ if (params.command === void 0) {
8889
+ throw invalidTerminalOptions(
8890
+ "Terminal options supplied args without command: arguments belong to a directly launched executable",
8891
+ { args: params.args.length }
8892
+ );
8893
+ }
8894
+ if (params.args.length > MAX_TERMINAL_ARGS) {
8895
+ throw invalidTerminalOptions(
8896
+ `Terminal options supplied ${params.args.length} args, at most ${MAX_TERMINAL_ARGS} are accepted`,
8897
+ { args: params.args.length }
8898
+ );
8899
+ }
8900
+ const index = params.args.findIndex(
8901
+ (arg) => typeof arg !== "string" || arg.includes("\0")
8902
+ );
8903
+ if (index !== -1) {
8904
+ throw invalidTerminalOptions(
8905
+ `Terminal options args[${index}] is not a NUL-free string; arguments are passed to process creation literally`,
8906
+ { index }
8907
+ );
8908
+ }
8909
+ }
8822
8910
  var SandboxTerminal = class {
8823
8911
  constructor(sandbox) {
8824
8912
  this.sandbox = sandbox;
8825
8913
  }
8826
8914
  sandbox;
8827
8915
  async create(params = {}) {
8916
+ assertTerminalOptions(params);
8828
8917
  const body5 = Object.fromEntries(
8829
8918
  Object.entries(params).filter(([, v]) => v !== void 0)
8830
8919
  );
@@ -9145,12 +9234,18 @@ var Sandbox = class _Sandbox {
9145
9234
  result.durationMs = response.duration_ms;
9146
9235
  result.duration_ms = response.duration_ms;
9147
9236
  }
9237
+ const timedOutRaw = response.timed_out ?? response.timedOut;
9238
+ if (typeof timedOutRaw === "boolean") {
9239
+ result.timedOut = timedOutRaw;
9240
+ result.timed_out = timedOutRaw;
9241
+ }
9148
9242
  return result;
9149
9243
  }
9150
9244
  async runCancellableExec(command, options) {
9151
9245
  let stdout = "";
9152
9246
  let stderr = "";
9153
9247
  let exitCode;
9248
+ let timedOut;
9154
9249
  for await (const event of this.execStream(command, options)) {
9155
9250
  if (event.type === "stdout" && typeof event.line === "string")
9156
9251
  stdout += event.line;
@@ -9158,6 +9253,7 @@ var Sandbox = class _Sandbox {
9158
9253
  stderr += event.line;
9159
9254
  if (event.type === "exit" && typeof event.exit_code === "number") {
9160
9255
  exitCode = event.exit_code;
9256
+ timedOut = event.timed_out;
9161
9257
  break;
9162
9258
  }
9163
9259
  }
@@ -9172,7 +9268,13 @@ var Sandbox = class _Sandbox {
9172
9268
  false
9173
9269
  );
9174
9270
  }
9175
- return { stdout, stderr, exitCode, exit_code: exitCode };
9271
+ return {
9272
+ stdout,
9273
+ stderr,
9274
+ exitCode,
9275
+ exit_code: exitCode,
9276
+ ...timedOut !== void 0 ? { timedOut, timed_out: timedOut } : {}
9277
+ };
9176
9278
  }
9177
9279
  execStream(command, options) {
9178
9280
  this.assertRunning("exec.stream");
@@ -9182,7 +9284,7 @@ var Sandbox = class _Sandbox {
9182
9284
  method: "POST",
9183
9285
  body: execBody(command, options),
9184
9286
  signal: options?.signal,
9185
- ...execDeadline(options)
9287
+ ...execStreamDeadline(options)
9186
9288
  }
9187
9289
  );
9188
9290
  return (async function* () {
@@ -9456,17 +9558,26 @@ var Sandbox = class _Sandbox {
9456
9558
  this.data = { ...this.data, ...data };
9457
9559
  return this;
9458
9560
  }
9561
+ /**
9562
+ * Resume a paused (or persistent-stopped) sandbox.
9563
+ *
9564
+ * Always sends an `Idempotency-Key`, generating a fresh one when the
9565
+ * caller doesn't supply one - same rationale as {@link generateIdempotencyKey}
9566
+ * for create(): the key is built once here and threaded through a single
9567
+ * `HttpClient.request` call, whose headers are fixed before its retry loop
9568
+ * runs, so every retry of *this* resume replays the same key. Without this,
9569
+ * a client-side retry of a slow resume could dispatch a second full resume
9570
+ * execution (re-running host reservation) instead of deduping server-side.
9571
+ */
9459
9572
  async resume(idempotencyKey11) {
9460
- const response = idempotencyKey11 ? await this.http.request(
9573
+ const key = idempotencyKey11 ?? generateIdempotencyKey();
9574
+ const response = await this.http.request(
9461
9575
  `/sandboxes/${this.id}/resume`,
9462
9576
  {
9463
9577
  method: "POST",
9464
9578
  body: {},
9465
- headers: { "Idempotency-Key": idempotencyKey11 }
9579
+ headers: { "Idempotency-Key": key }
9466
9580
  }
9467
- ) : await this.http.post(
9468
- `/sandboxes/${this.id}/resume`,
9469
- {}
9470
9581
  );
9471
9582
  const data = unwrap48(response);
9472
9583
  this.data = { ...this.data, ...data };
@@ -9805,6 +9916,50 @@ var Sandbox = class _Sandbox {
9805
9916
  }
9806
9917
  }
9807
9918
  };
9919
+ function sleep8(ms) {
9920
+ return new Promise((resolve) => setTimeout(resolve, ms));
9921
+ }
9922
+ function isResumableState(state) {
9923
+ return state === "paused" || state === "stopped";
9924
+ }
9925
+ var DEFAULT_PAUSING_WAIT_TIMEOUT_SEC = 30;
9926
+ var PAUSING_POLL_INTERVAL_MS = 500;
9927
+ var RESUME_RETRY_MAX_ATTEMPTS = 5;
9928
+ var RESUME_RETRY_FALLBACK_DELAY_MS = 500;
9929
+ async function waitOutPausingState(sandbox, timeoutSec) {
9930
+ const deadline = Date.now() + timeoutSec * 1e3;
9931
+ while (sandbox.state === "pausing" && Date.now() < deadline) {
9932
+ await sleep8(PAUSING_POLL_INTERVAL_MS);
9933
+ try {
9934
+ await sandbox.refresh();
9935
+ } catch {
9936
+ }
9937
+ }
9938
+ }
9939
+ async function resumeWithRetryHint(sandbox) {
9940
+ for (let attempt = 0; ; attempt++) {
9941
+ try {
9942
+ await sandbox.resume();
9943
+ return;
9944
+ } catch (error) {
9945
+ const canRetry = error instanceof MiosaError && error.status === 409 && error.retryable === true && attempt < RESUME_RETRY_MAX_ATTEMPTS;
9946
+ if (!canRetry) throw error;
9947
+ await sleep8(error.retryAfterMs ?? RESUME_RETRY_FALLBACK_DELAY_MS);
9948
+ }
9949
+ }
9950
+ }
9951
+ async function resumeForGetOrCreate(sandbox, params) {
9952
+ if (params.resume === false) return;
9953
+ if (sandbox.state === "pausing") {
9954
+ await waitOutPausingState(
9955
+ sandbox,
9956
+ params.pausingWaitTimeoutSec ?? DEFAULT_PAUSING_WAIT_TIMEOUT_SEC
9957
+ );
9958
+ }
9959
+ if (isResumableState(sandbox.state)) {
9960
+ await resumeWithRetryHint(sandbox);
9961
+ }
9962
+ }
9808
9963
  var Sandboxes = class {
9809
9964
  constructor(http) {
9810
9965
  this.http = http;
@@ -9900,9 +10055,14 @@ var Sandboxes = class {
9900
10055
  /**
9901
10056
  * Idempotently obtain a persistent sandbox by stable name.
9902
10057
  *
9903
- * If the sandbox exists, it is returned. If it is paused and `resume !== false`,
9904
- * it is resumed first. If it does not exist, it is created with the supplied
9905
- * params. Destroyed sandboxes are permanent and are not resumed by this helper.
10058
+ * If the sandbox exists, it is returned. Unless `resume === false`: a
10059
+ * `paused` or persistent `stopped` sandbox is resumed; a `pausing`
10060
+ * sandbox is waited out (bounded by `pausingWaitTimeoutSec`) and then
10061
+ * resumed if it landed on `paused`/`stopped`; a resume answered with a
10062
+ * 409 the server marks `retryable: true` (e.g. a concurrent pause still
10063
+ * finishing) is retried using the server's `retry_after_ms` hint. If the
10064
+ * sandbox does not exist, it is created with the supplied params.
10065
+ * Destroyed sandboxes are permanent and are not resumed by this helper.
9906
10066
  */
9907
10067
  async getOrCreate(params) {
9908
10068
  let sandbox;
@@ -9912,9 +10072,7 @@ var Sandboxes = class {
9912
10072
  if (!(error instanceof NotFoundError)) throw error;
9913
10073
  sandbox = await this.create(params);
9914
10074
  }
9915
- if (sandbox.state === "paused" && params.resume !== false) {
9916
- await sandbox.resume();
9917
- }
10075
+ await resumeForGetOrCreate(sandbox, params);
9918
10076
  if (params.waitUntilReady === true) {
9919
10077
  const ready = await sandbox.waitUntilReady({
9920
10078
  timeout: params.waitTimeoutSec ?? 60
@@ -11025,10 +11183,1101 @@ var WorkspaceMembers = class {
11025
11183
  }
11026
11184
  };
11027
11185
 
11186
+ // src/runner/region.ts
11187
+ var DEFAULT_RUNNER_REGION = "us";
11188
+ var KNOWN_RUNNER_REGIONS = /* @__PURE__ */ new Set();
11189
+ function regionFromApiKey(apiKey) {
11190
+ const parts = apiKey.split("_");
11191
+ const candidate = parts[1];
11192
+ if (parts.length >= 3 && candidate !== void 0 && KNOWN_RUNNER_REGIONS.has(candidate)) {
11193
+ return candidate;
11194
+ }
11195
+ return DEFAULT_RUNNER_REGION;
11196
+ }
11197
+ var DEFAULT_RUNNER_BASE_DOMAIN = "miosa.ai";
11198
+ function regionHostname(region, baseDomain = DEFAULT_RUNNER_BASE_DOMAIN) {
11199
+ return `run-${region}.${baseDomain}`;
11200
+ }
11201
+ function tagHostname(region, tag, baseDomain = DEFAULT_RUNNER_BASE_DOMAIN) {
11202
+ return `${tag}.${regionHostname(region, baseDomain)}`;
11203
+ }
11204
+ function sandboxTag(sandboxId) {
11205
+ const first = sandboxId.charAt(0);
11206
+ if (!first) {
11207
+ throw new TypeError("sandboxTag: sandboxId must not be empty");
11208
+ }
11209
+ return first.toLowerCase();
11210
+ }
11211
+ var DnsAddressResolver = class {
11212
+ constructor(ttlMs = 6e4) {
11213
+ this.ttlMs = ttlMs;
11214
+ }
11215
+ ttlMs;
11216
+ cache = /* @__PURE__ */ new Map();
11217
+ async resolve(hostname) {
11218
+ if (isIP(hostname) !== 0) return [hostname];
11219
+ if (hostname === "localhost") return ["127.0.0.1"];
11220
+ const cached = this.cache.get(hostname);
11221
+ if (cached && cached.expiresAt > Date.now()) return cached.addresses;
11222
+ const addresses = await resolve4(hostname);
11223
+ if (addresses.length === 0) {
11224
+ throw new Error(`DnsAddressResolver: ${hostname} resolved to zero A records`);
11225
+ }
11226
+ this.cache.set(hostname, { addresses, expiresAt: Date.now() + this.ttlMs });
11227
+ return addresses;
11228
+ }
11229
+ };
11230
+ var StaticAddressResolver = class {
11231
+ constructor(table) {
11232
+ this.table = table;
11233
+ }
11234
+ table;
11235
+ async resolve(hostname) {
11236
+ const addresses = this.table.get(hostname);
11237
+ if (!addresses || addresses.length === 0) {
11238
+ throw new Error(`StaticAddressResolver: no addresses configured for ${hostname}`);
11239
+ }
11240
+ return addresses;
11241
+ }
11242
+ };
11243
+
11244
+ // src/runner/transport.ts
11245
+ var RunnerTimeoutError = class extends Error {
11246
+ constructor(hostname, path, timeoutMs) {
11247
+ super(`runner request to ${hostname}${path} timed out after ${timeoutMs}ms`);
11248
+ this.name = "RunnerTimeoutError";
11249
+ }
11250
+ };
11251
+ var RunnerConnectError = class extends Error {
11252
+ constructor(hostname, address, cause) {
11253
+ super(
11254
+ `runner transport failed to connect to ${hostname} (${address}): ${cause instanceof Error ? cause.message : String(cause)}`,
11255
+ { cause }
11256
+ );
11257
+ this.name = "RunnerConnectError";
11258
+ }
11259
+ };
11260
+ function isConnectFailure(error) {
11261
+ if (error instanceof RunnerConnectError || error instanceof RunnerTimeoutError) {
11262
+ return true;
11263
+ }
11264
+ if (error && typeof error === "object" && "code" in error) {
11265
+ const code = error.code;
11266
+ return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "EHOSTUNREACH" || code === "ENETUNREACH" || code === "ERR_HTTP2_GOAWAY_SESSION";
11267
+ }
11268
+ return false;
11269
+ }
11270
+
11271
+ // src/runner/http2-transport.ts
11272
+ function markHandled(promise) {
11273
+ promise.catch(() => void 0);
11274
+ return promise;
11275
+ }
11276
+ var DEFAULT_SESSIONS_PER_ADDRESS = 8;
11277
+ var DEFAULT_TIMEOUT_MS = 3e4;
11278
+ var Http2Transport = class {
11279
+ protocol = "h2";
11280
+ defaultSessionsPerAddress;
11281
+ tls;
11282
+ port;
11283
+ resolver;
11284
+ defaultTimeoutMs;
11285
+ pools = /* @__PURE__ */ new Map();
11286
+ constructor(options = {}) {
11287
+ this.defaultSessionsPerAddress = options.defaultSessionsPerAddress ?? DEFAULT_SESSIONS_PER_ADDRESS;
11288
+ this.tls = options.tls ?? true;
11289
+ this.port = options.port ?? (this.tls ? 443 : 80);
11290
+ this.resolver = options.resolver ?? new DnsAddressResolver();
11291
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
11292
+ }
11293
+ prewarm(regionHostname2, sessionsPerAddress) {
11294
+ this.poolFor(regionHostname2, sessionsPerAddress);
11295
+ }
11296
+ async addressCount(regionHostname2) {
11297
+ const addresses = await this.poolFor(regionHostname2).addresses;
11298
+ return addresses.length;
11299
+ }
11300
+ async ready(regionHostname2, addressIndex) {
11301
+ await this.acquireSession(regionHostname2, addressIndex);
11302
+ }
11303
+ async request(regionHostname2, addressIndex, authority, path, init) {
11304
+ const { session } = await this.acquireSession(regionHostname2, addressIndex);
11305
+ return this.sendRequest(regionHostname2, authority, path, session, init);
11306
+ }
11307
+ async streamRequest(regionHostname2, addressIndex, authority, path, init) {
11308
+ const { session } = await this.acquireSession(regionHostname2, addressIndex);
11309
+ return this.sendStreamRequest(authority, path, session, init);
11310
+ }
11311
+ /** Shared by request() and ready() - the latter just discards the session. */
11312
+ async acquireSession(regionHostname2, addressIndex) {
11313
+ const pool = this.poolFor(regionHostname2);
11314
+ const addresses = await pool.addresses;
11315
+ const n = addresses.length;
11316
+ const addrIdx = (addressIndex % n + n) % n;
11317
+ const address = addresses[addrIdx];
11318
+ const groups = await pool.groups;
11319
+ const group = groups[addrIdx];
11320
+ if (group.length === 0) {
11321
+ throw new Error(
11322
+ `Http2Transport: ${regionHostname2} address ${addrIdx} has zero sessions configured (prewarm/request with sessionsPerAddress > 0 first)`
11323
+ );
11324
+ }
11325
+ const cursor = pool.nextSlot.get(addrIdx) ?? 0;
11326
+ pool.nextSlot.set(addrIdx, (cursor + 1) % group.length);
11327
+ let session;
11328
+ try {
11329
+ session = await group[cursor];
11330
+ } catch (error) {
11331
+ throw new RunnerConnectError(regionHostname2, address, error);
11332
+ }
11333
+ if (session.closed || session.destroyed) {
11334
+ const fresh = this.connectSession(regionHostname2, address);
11335
+ group[cursor] = fresh;
11336
+ try {
11337
+ session = await fresh;
11338
+ } catch (error) {
11339
+ throw new RunnerConnectError(regionHostname2, address, error);
11340
+ }
11341
+ }
11342
+ return { session };
11343
+ }
11344
+ async close() {
11345
+ const pools = [...this.pools.values()];
11346
+ this.pools.clear();
11347
+ for (const pool of pools) {
11348
+ const groups = await pool.groups.catch(() => []);
11349
+ for (const group of groups) {
11350
+ for (const slot of group) {
11351
+ try {
11352
+ const session = await slot;
11353
+ if (!session.closed) session.close();
11354
+ } catch {
11355
+ }
11356
+ }
11357
+ }
11358
+ }
11359
+ }
11360
+ poolFor(regionHostname2, sessionsPerAddress) {
11361
+ let pool = this.pools.get(regionHostname2);
11362
+ if (!pool) {
11363
+ pool = this.buildPool(regionHostname2, sessionsPerAddress ?? this.defaultSessionsPerAddress);
11364
+ this.pools.set(regionHostname2, pool);
11365
+ }
11366
+ return pool;
11367
+ }
11368
+ buildPool(regionHostname2, sessionsPerAddress) {
11369
+ const addresses = this.resolver.resolve(regionHostname2);
11370
+ const groups = addresses.then(
11371
+ (resolved) => resolved.map(
11372
+ (address) => (
11373
+ // sessionsPerAddress: 0 is a deliberate "open nothing" (e.g. a
11374
+ // count-only probe).
11375
+ Array.from(
11376
+ { length: sessionsPerAddress },
11377
+ () => markHandled(this.connectSession(regionHostname2, address))
11378
+ )
11379
+ )
11380
+ )
11381
+ );
11382
+ markHandled(addresses);
11383
+ markHandled(groups);
11384
+ return { addresses, groups, nextSlot: /* @__PURE__ */ new Map() };
11385
+ }
11386
+ connectSession(regionHostname2, address) {
11387
+ return new Promise((resolve, reject) => {
11388
+ const authority = `${this.tls ? "https" : "http"}://${address}:${this.port}`;
11389
+ const connectOptions = this.tls ? { servername: regionHostname2 } : {};
11390
+ const session = http2.connect(authority, connectOptions);
11391
+ const onError = (error) => {
11392
+ session.destroy();
11393
+ reject(error);
11394
+ };
11395
+ session.once("error", onError);
11396
+ session.once("connect", () => {
11397
+ session.removeListener("error", onError);
11398
+ session.on("error", () => void 0);
11399
+ resolve(session);
11400
+ });
11401
+ });
11402
+ }
11403
+ sendRequest(regionHostname2, authority, path, session, init) {
11404
+ const timeoutMs = init.timeoutMs ?? this.defaultTimeoutMs;
11405
+ return new Promise((resolve, reject) => {
11406
+ const requestHeaders = {
11407
+ ":method": init.method,
11408
+ ":path": path,
11409
+ ":authority": authority,
11410
+ ...init.headers
11411
+ };
11412
+ const stream = session.request(requestHeaders);
11413
+ let status = 0;
11414
+ const responseHeaders = {};
11415
+ const chunks = [];
11416
+ let settled = false;
11417
+ const timer = setTimeout(() => {
11418
+ if (settled) return;
11419
+ settled = true;
11420
+ stream.close(http2.constants.NGHTTP2_CANCEL);
11421
+ reject(new RunnerTimeoutError(regionHostname2, path, timeoutMs));
11422
+ }, timeoutMs);
11423
+ const onAbort = () => {
11424
+ if (settled) return;
11425
+ settled = true;
11426
+ clearTimeout(timer);
11427
+ stream.close(http2.constants.NGHTTP2_CANCEL);
11428
+ reject(new DOMException("runner request aborted", "AbortError"));
11429
+ };
11430
+ init.signal?.addEventListener("abort", onAbort, { once: true });
11431
+ stream.on("response", (headers) => {
11432
+ status = Number(headers[":status"] ?? 0);
11433
+ for (const [key, value] of Object.entries(headers)) {
11434
+ if (key.startsWith(":") || value === void 0) continue;
11435
+ responseHeaders[key] = Array.isArray(value) ? value.join(", ") : String(value);
11436
+ }
11437
+ });
11438
+ stream.on("data", (chunk) => chunks.push(chunk));
11439
+ stream.on("end", () => {
11440
+ if (settled) return;
11441
+ settled = true;
11442
+ clearTimeout(timer);
11443
+ init.signal?.removeEventListener("abort", onAbort);
11444
+ resolve({ status, headers: responseHeaders, body: Buffer.concat(chunks).toString("utf8") });
11445
+ });
11446
+ stream.on("error", (error) => {
11447
+ if (settled) return;
11448
+ settled = true;
11449
+ clearTimeout(timer);
11450
+ init.signal?.removeEventListener("abort", onAbort);
11451
+ reject(error);
11452
+ });
11453
+ if (init.body !== void 0) {
11454
+ stream.end(init.body);
11455
+ } else {
11456
+ stream.end();
11457
+ }
11458
+ });
11459
+ }
11460
+ sendStreamRequest(authority, path, session, init) {
11461
+ return new Promise((resolve, reject) => {
11462
+ const requestHeaders = {
11463
+ ":method": init.method,
11464
+ ":path": path,
11465
+ ":authority": authority,
11466
+ ...init.headers
11467
+ };
11468
+ const stream = session.request(requestHeaders);
11469
+ let settled = false;
11470
+ stream.once("response", (headers) => {
11471
+ if (settled) return;
11472
+ settled = true;
11473
+ const responseHeaders = {};
11474
+ for (const [key, value] of Object.entries(headers)) {
11475
+ if (key.startsWith(":") || value === void 0) continue;
11476
+ responseHeaders[key] = Array.isArray(value) ? value.join(", ") : String(value);
11477
+ }
11478
+ resolve({
11479
+ status: Number(headers[":status"] ?? 0),
11480
+ headers: responseHeaders,
11481
+ chunks: textChunks(stream)
11482
+ });
11483
+ });
11484
+ stream.once("error", (error) => {
11485
+ if (settled) return;
11486
+ settled = true;
11487
+ reject(error);
11488
+ });
11489
+ if (init.body !== void 0) {
11490
+ stream.end(init.body);
11491
+ } else {
11492
+ stream.end();
11493
+ }
11494
+ });
11495
+ }
11496
+ };
11497
+ async function* textChunks(stream) {
11498
+ const decoder = new TextDecoder();
11499
+ try {
11500
+ for await (const chunk of stream) {
11501
+ yield decoder.decode(chunk, { stream: true });
11502
+ }
11503
+ } finally {
11504
+ if (!stream.closed) stream.close();
11505
+ }
11506
+ }
11507
+ function markHandled2(promise) {
11508
+ promise.catch(() => void 0);
11509
+ return promise;
11510
+ }
11511
+ var cachedNative;
11512
+ function loadNative() {
11513
+ if (cachedNative !== void 0) return cachedNative;
11514
+ try {
11515
+ const nodeRequire = createRequire(import.meta.url);
11516
+ const candidates = ["../native/h3-runner/index.js", "../../native/h3-runner/index.js"];
11517
+ let native = null;
11518
+ for (const specifier of candidates) {
11519
+ try {
11520
+ native = nodeRequire(specifier);
11521
+ if (native) break;
11522
+ } catch {
11523
+ }
11524
+ }
11525
+ if (!native) {
11526
+ cachedNative = null;
11527
+ return null;
11528
+ }
11529
+ cachedNative = native.available ? native : null;
11530
+ } catch {
11531
+ cachedNative = null;
11532
+ }
11533
+ return cachedNative;
11534
+ }
11535
+ function detectHttp3Support() {
11536
+ const native = loadNative();
11537
+ if (native) {
11538
+ return {
11539
+ available: true,
11540
+ reason: `native HTTP/3 addon loaded (${native.platformArchKey()})`
11541
+ };
11542
+ }
11543
+ return {
11544
+ available: false,
11545
+ reason: `no native HTTP/3 addon for this platform/architecture (Node ${process.version}, see native/h3-runner/README.md for which platforms are built). Falling back to HTTP/2.`
11546
+ };
11547
+ }
11548
+ function resetHttp3DetectionCache() {
11549
+ cachedNative = void 0;
11550
+ }
11551
+ var DEFAULT_SESSIONS_PER_ADDRESS2 = 4;
11552
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
11553
+ var DEFAULT_CONNECT_TIMEOUT_MS = 2e3;
11554
+ var Http3Transport = class {
11555
+ protocol = "h3";
11556
+ native;
11557
+ defaultSessionsPerAddress;
11558
+ resolver;
11559
+ defaultTimeoutMs;
11560
+ connectTimeoutMs;
11561
+ insecure;
11562
+ port;
11563
+ pools = /* @__PURE__ */ new Map();
11564
+ constructor(options = {}) {
11565
+ const native = loadNative();
11566
+ if (!native) {
11567
+ throw new Error("Http3Transport: no native HTTP/3 addon available on this platform");
11568
+ }
11569
+ this.native = native;
11570
+ this.defaultSessionsPerAddress = options.defaultSessionsPerAddress ?? DEFAULT_SESSIONS_PER_ADDRESS2;
11571
+ this.resolver = options.resolver ?? new DnsAddressResolver();
11572
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS2;
11573
+ this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
11574
+ this.insecure = options.insecure ?? false;
11575
+ this.port = options.port ?? 443;
11576
+ }
11577
+ prewarm(regionHostname2, sessionsPerAddress) {
11578
+ this.poolFor(regionHostname2, sessionsPerAddress);
11579
+ }
11580
+ async addressCount(regionHostname2) {
11581
+ const addresses = await this.poolFor(regionHostname2).addresses;
11582
+ return addresses.length;
11583
+ }
11584
+ async ready(regionHostname2, addressIndex) {
11585
+ await this.acquireHandle(regionHostname2, addressIndex);
11586
+ }
11587
+ async request(regionHostname2, addressIndex, authority, path, init) {
11588
+ const { address, handle, reconnect } = await this.acquireHandle(regionHostname2, addressIndex);
11589
+ return this.sendRequest(regionHostname2, address, authority, path, handle, init, reconnect);
11590
+ }
11591
+ /**
11592
+ * The native addon has no streaming request support yet - buffers the
11593
+ * whole response (same as request()) and hands it back as a single
11594
+ * chunk. Correct, not incrementally streamed; see transport.ts's doc
11595
+ * comment on streamRequest.
11596
+ */
11597
+ async streamRequest(regionHostname2, addressIndex, authority, path, init) {
11598
+ const response = await this.request(regionHostname2, addressIndex, authority, path, init);
11599
+ return { status: response.status, headers: response.headers, chunks: singleChunk(response.body) };
11600
+ }
11601
+ /** Shared by request() and ready() - the latter just discards the handle. */
11602
+ async acquireHandle(regionHostname2, addressIndex) {
11603
+ const pool = this.poolFor(regionHostname2);
11604
+ const addresses = await pool.addresses;
11605
+ const n = addresses.length;
11606
+ const addrIdx = (addressIndex % n + n) % n;
11607
+ const address = addresses[addrIdx];
11608
+ const groups = await pool.groups;
11609
+ const group = groups[addrIdx];
11610
+ if (group.length === 0) {
11611
+ throw new Error(
11612
+ `Http3Transport: ${regionHostname2} address ${addrIdx} has zero sessions configured (prewarm/request with sessionsPerAddress > 0 first)`
11613
+ );
11614
+ }
11615
+ const cursor = pool.nextSlot.get(addrIdx) ?? 0;
11616
+ pool.nextSlot.set(addrIdx, (cursor + 1) % group.length);
11617
+ let handle;
11618
+ try {
11619
+ handle = await group[cursor];
11620
+ } catch (error) {
11621
+ throw new RunnerConnectError(regionHostname2, address, error);
11622
+ }
11623
+ const reconnect = () => {
11624
+ const fresh = this.connectSession(regionHostname2, address);
11625
+ group[cursor] = fresh;
11626
+ return fresh;
11627
+ };
11628
+ return { address, handle, reconnect };
11629
+ }
11630
+ async close() {
11631
+ const pools = [...this.pools.values()];
11632
+ this.pools.clear();
11633
+ for (const pool of pools) {
11634
+ const groups = await pool.groups.catch(() => []);
11635
+ for (const group of groups) {
11636
+ for (const slot of group) {
11637
+ try {
11638
+ const handle = await slot;
11639
+ await this.native.h3Close(handle);
11640
+ } catch {
11641
+ }
11642
+ }
11643
+ }
11644
+ }
11645
+ }
11646
+ poolFor(regionHostname2, sessionsPerAddress) {
11647
+ let pool = this.pools.get(regionHostname2);
11648
+ if (!pool) {
11649
+ pool = this.buildPool(regionHostname2, sessionsPerAddress ?? this.defaultSessionsPerAddress);
11650
+ this.pools.set(regionHostname2, pool);
11651
+ }
11652
+ return pool;
11653
+ }
11654
+ buildPool(regionHostname2, sessionsPerAddress) {
11655
+ const addresses = this.resolver.resolve(regionHostname2);
11656
+ const groups = addresses.then(
11657
+ (resolved) => resolved.map(
11658
+ (address) => Array.from(
11659
+ { length: sessionsPerAddress },
11660
+ () => markHandled2(this.connectSession(regionHostname2, address))
11661
+ )
11662
+ )
11663
+ );
11664
+ markHandled2(addresses);
11665
+ markHandled2(groups);
11666
+ return { addresses, groups, nextSlot: /* @__PURE__ */ new Map() };
11667
+ }
11668
+ connectSession(regionHostname2, address) {
11669
+ return this.native.h3Connect(address, regionHostname2, this.port, this.insecure, this.connectTimeoutMs);
11670
+ }
11671
+ async sendRequest(regionHostname2, address, authority, path, handle, init, reconnect) {
11672
+ const timeoutMs = init.timeoutMs ?? this.defaultTimeoutMs;
11673
+ const body5 = init.body !== void 0 ? Buffer.from(init.body) : void 0;
11674
+ const attempt = async (activeHandle) => {
11675
+ const requestPromise = this.native.h3Request(
11676
+ activeHandle,
11677
+ init.method,
11678
+ authority,
11679
+ path,
11680
+ init.headers ?? {},
11681
+ body5
11682
+ );
11683
+ const result = await raceWithTimeout(
11684
+ requestPromise,
11685
+ timeoutMs,
11686
+ new RunnerTimeoutError(regionHostname2, path, timeoutMs)
11687
+ );
11688
+ return { status: result.status, headers: result.headers, body: result.body.toString("utf8") };
11689
+ };
11690
+ try {
11691
+ return await attempt(handle);
11692
+ } catch (error) {
11693
+ let fresh;
11694
+ try {
11695
+ fresh = await reconnect();
11696
+ } catch (reconnectError) {
11697
+ throw new RunnerConnectError(regionHostname2, address, reconnectError);
11698
+ }
11699
+ return attempt(fresh);
11700
+ }
11701
+ }
11702
+ };
11703
+ function raceWithTimeout(promise, timeoutMs, timeoutError) {
11704
+ return new Promise((resolve, reject) => {
11705
+ const timer = setTimeout(() => reject(timeoutError), timeoutMs);
11706
+ promise.then(
11707
+ (value) => {
11708
+ clearTimeout(timer);
11709
+ resolve(value);
11710
+ },
11711
+ (error) => {
11712
+ clearTimeout(timer);
11713
+ reject(error);
11714
+ }
11715
+ );
11716
+ });
11717
+ }
11718
+ async function* singleChunk(body5) {
11719
+ if (body5) yield body5;
11720
+ }
11721
+
11722
+ // src/runner/transport-factory.ts
11723
+ function createBestTransport(options = {}) {
11724
+ const http3 = detectHttp3Support();
11725
+ if (!http3.available) {
11726
+ return { transport: new Http2Transport(options.http2), http3 };
11727
+ }
11728
+ const transport = new RacingTransport(new Http2Transport(options.http2), new Http3Transport(options.http3));
11729
+ return { transport, http3 };
11730
+ }
11731
+ var RacingTransport = class {
11732
+ constructor(http22, http3) {
11733
+ this.http2 = http22;
11734
+ this.http3 = http3;
11735
+ }
11736
+ http2;
11737
+ http3;
11738
+ // Aspirational label for a transport that may use either protocol per
11739
+ // host - perHostProtocol() is the ground truth once a host has decided.
11740
+ protocol = "h3";
11741
+ protocolByHost = /* @__PURE__ */ new Map();
11742
+ watching = /* @__PURE__ */ new Set();
11743
+ /** The protocol a region host is currently using - "h2" until HTTP/3 proves itself, per the class doc comment. */
11744
+ perHostProtocol(regionHostname2) {
11745
+ return this.protocolByHost.get(regionHostname2) ?? "h2";
11746
+ }
11747
+ prewarm(regionHostname2, sessionsPerAddress) {
11748
+ this.http2.prewarm(regionHostname2, sessionsPerAddress);
11749
+ this.http3.prewarm(regionHostname2, sessionsPerAddress);
11750
+ this.watchHttp3(regionHostname2);
11751
+ }
11752
+ addressCount(regionHostname2) {
11753
+ return this.http2.addressCount(regionHostname2);
11754
+ }
11755
+ async ready(regionHostname2, addressIndex) {
11756
+ await this.transportFor(regionHostname2).ready(regionHostname2, addressIndex);
11757
+ }
11758
+ request(regionHostname2, addressIndex, authority, path, init) {
11759
+ return this.transportFor(regionHostname2).request(regionHostname2, addressIndex, authority, path, init);
11760
+ }
11761
+ streamRequest(regionHostname2, addressIndex, authority, path, init) {
11762
+ return this.transportFor(regionHostname2).streamRequest(regionHostname2, addressIndex, authority, path, init);
11763
+ }
11764
+ async close() {
11765
+ await Promise.all([this.http2.close(), this.http3.close()]);
11766
+ }
11767
+ /** Whatever protocol is decided for regionHostname right now - never waits. */
11768
+ transportFor(regionHostname2) {
11769
+ this.watchHttp3(regionHostname2);
11770
+ return this.perHostProtocol(regionHostname2) === "h3" ? this.http3 : this.http2;
11771
+ }
11772
+ /**
11773
+ * Starts (once per host) a background wait on HTTP/3's own readiness
11774
+ * for address 0 - connection setup, not a request - and promotes the
11775
+ * host to "h3" the moment it resolves. Never awaited by any caller;
11776
+ * purely a side effect that later `transportFor` calls observe.
11777
+ */
11778
+ watchHttp3(regionHostname2) {
11779
+ if (this.watching.has(regionHostname2)) return;
11780
+ this.watching.add(regionHostname2);
11781
+ this.http3.ready(regionHostname2, 0).then(
11782
+ () => this.protocolByHost.set(regionHostname2, "h3"),
11783
+ () => this.protocolByHost.set(regionHostname2, "h2")
11784
+ // failed outright - stay on HTTP/2 for this host
11785
+ );
11786
+ }
11787
+ };
11788
+
11789
+ // src/runner/errors.ts
11790
+ var RunnerError = class _RunnerError extends Error {
11791
+ status;
11792
+ code;
11793
+ runnerUrl;
11794
+ /** Only meaningful for the nested "platform"/envelope shape - undefined for the flat C2 shape. */
11795
+ retryable;
11796
+ constructor(status, code, options = {}) {
11797
+ super(options.message ?? `runner request failed with ${status} ${code}`);
11798
+ this.name = "RunnerError";
11799
+ this.status = status;
11800
+ this.code = code;
11801
+ this.runnerUrl = options.runnerUrl;
11802
+ this.retryable = options.retryable;
11803
+ }
11804
+ static fromResponse(status, body5) {
11805
+ let parsed = {};
11806
+ try {
11807
+ parsed = JSON.parse(body5);
11808
+ } catch {
11809
+ }
11810
+ if (typeof parsed.error === "string") {
11811
+ return new _RunnerError(status, parsed.error, { runnerUrl: parsed.runner_url });
11812
+ }
11813
+ if (parsed.error && typeof parsed.error === "object") {
11814
+ return new _RunnerError(status, parsed.error.code || `http_${status}`, {
11815
+ message: parsed.error.message,
11816
+ retryable: parsed.error.retryable
11817
+ });
11818
+ }
11819
+ return new _RunnerError(status, `http_${status}`);
11820
+ }
11821
+ get isMisdirected() {
11822
+ return this.status === 421;
11823
+ }
11824
+ /**
11825
+ * C2 (revised): admission-cap-reached ("runtime_busy") moved from 503 to
11826
+ * 429 with `Retry-After: 0`; `feed_stale` stayed on 503. Checked by
11827
+ * code, not status, so this stays correct regardless of which of the
11828
+ * two carries it.
11829
+ */
11830
+ get isBusy() {
11831
+ return this.code === "runtime_busy" || this.code === "feed_stale";
11832
+ }
11833
+ /** C2: the runner doesn't serve this create shape at all - send it to api.miosa.ai instead. */
11834
+ get isUnsupportedRequest() {
11835
+ return this.code === "RUNNER_UNSUPPORTED_REQUEST";
11836
+ }
11837
+ };
11838
+
11839
+ // src/runner/envelope.ts
11840
+ function encodeSomaBytes(content) {
11841
+ const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
11842
+ const maybeBuffer = globalThis.Buffer;
11843
+ if (maybeBuffer) return maybeBuffer.from(bytes).toString("base64");
11844
+ let bin = "";
11845
+ for (const byte of bytes) bin += String.fromCharCode(byte);
11846
+ return btoa(bin);
11847
+ }
11848
+ function decodeSomaBytes(bytes) {
11849
+ const maybeBuffer = globalThis.Buffer;
11850
+ if (maybeBuffer) return maybeBuffer.from(bytes.data, "base64");
11851
+ const binary = atob(bytes.data);
11852
+ const out = new Uint8Array(binary.length);
11853
+ for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
11854
+ return out;
11855
+ }
11856
+ function hyphenateInstanceId(instanceId) {
11857
+ if (instanceId.length !== 32 || instanceId.includes("-")) return instanceId;
11858
+ return [instanceId.slice(0, 8), instanceId.slice(8, 12), instanceId.slice(12, 16), instanceId.slice(16, 20), instanceId.slice(20)].join(
11859
+ "-"
11860
+ );
11861
+ }
11862
+ function unwrapSomaEnvelope(httpStatus, body5) {
11863
+ let envelope = {};
11864
+ try {
11865
+ envelope = JSON.parse(body5 || "{}");
11866
+ } catch {
11867
+ throw new RunnerError(httpStatus, `http_${httpStatus}`, {
11868
+ message: "runner response was not a valid soma.api.v1 envelope"
11869
+ });
11870
+ }
11871
+ if (envelope.status === "error" || envelope.result === null || envelope.result === void 0) {
11872
+ const detail = envelope.error;
11873
+ throw new RunnerError(httpStatus, detail?.code || `http_${httpStatus}`, {
11874
+ message: detail?.message,
11875
+ retryable: detail?.retryable
11876
+ });
11877
+ }
11878
+ return envelope.result;
11879
+ }
11880
+
11881
+ // src/runner/runner-client.ts
11882
+ var DEFAULT_SESSIONS_PER_ADDRESS3 = 8;
11883
+ function createSupportedByRunner(params) {
11884
+ const hasTemplate = params["template_id"] !== void 0 || params["templateId"] !== void 0 || params["image"] !== void 0;
11885
+ const hasSnapshot = params["snapshot_id"] !== void 0 || params["snapshotId"] !== void 0 || params["source_snapshot_path"] !== void 0 || params["sourceSnapshotPath"] !== void 0;
11886
+ const hasCwd = params["cwd"] !== void 0 || params["entrypoint"] !== void 0;
11887
+ const isPersistent = params["persistent"] === true || params["always_on"] === true || params["alwaysOn"] === true;
11888
+ const size = params["size"];
11889
+ const isNonXs = size !== void 0 && size !== "xs";
11890
+ return !(hasTemplate || hasSnapshot || hasCwd || isPersistent || isNonXs);
11891
+ }
11892
+ var RunnerClient = class {
11893
+ region;
11894
+ hostname;
11895
+ baseDomain;
11896
+ apiKey;
11897
+ sessionsPerAddress;
11898
+ transport;
11899
+ fallbackCreate;
11900
+ /** tag -> which of the region's resolved addresses answers for it, learned from /healthz. */
11901
+ addressIndexByTag = /* @__PURE__ */ new Map();
11902
+ /** sandbox id -> tag, set only once a 421 corrects the id-derived guess. */
11903
+ correctedTag = /* @__PURE__ */ new Map();
11904
+ constructor(options) {
11905
+ this.apiKey = options.apiKey;
11906
+ this.region = options.region ?? regionFromApiKey(options.apiKey);
11907
+ this.baseDomain = options.baseDomain ?? DEFAULT_RUNNER_BASE_DOMAIN;
11908
+ this.hostname = regionHostname(this.region, this.baseDomain);
11909
+ this.sessionsPerAddress = options.sessionsPerAddress ?? DEFAULT_SESSIONS_PER_ADDRESS3;
11910
+ this.transport = options.transport ?? createBestTransport().transport;
11911
+ this.fallbackCreate = options.fallbackCreate;
11912
+ this.transport.prewarm(this.hostname, this.sessionsPerAddress);
11913
+ void this.discoverTags();
11914
+ }
11915
+ get protocol() {
11916
+ return this.transport.protocol;
11917
+ }
11918
+ /** The tag a 421 has corrected sandboxId to, if any. */
11919
+ correctedTagFor(sandboxId) {
11920
+ return this.correctedTag.get(sandboxId);
11921
+ }
11922
+ async healthzByIndex(addressIndex) {
11923
+ const res = await this.transport.request(this.hostname, addressIndex, this.hostname, "/healthz", {
11924
+ method: "GET"
11925
+ });
11926
+ return JSON.parse(res.body || "{}");
11927
+ }
11928
+ /**
11929
+ * POST /api/v1/sandboxes against the region hostname. Create shapes the
11930
+ * runner doesn't serve (C2) go straight to `fallbackCreate` without
11931
+ * ever reaching the runner; a `400 RUNNER_UNSUPPORTED_REQUEST` the
11932
+ * runner returns anyway falls back the same way, once. On `503
11933
+ * runtime_busy`/`feed_stale` or a connect failure, retries on the next
11934
+ * resolved IP, at most n-1 times (C6).
11935
+ */
11936
+ async createSandbox(params = {}, opts = {}) {
11937
+ if (!createSupportedByRunner(params)) {
11938
+ return this.createViaFallback(params, opts);
11939
+ }
11940
+ const n = await this.transport.addressCount(this.hostname);
11941
+ if (n === 0) {
11942
+ throw new Error(`RunnerClient: no runner addresses for ${this.hostname}`);
11943
+ }
11944
+ let lastError;
11945
+ for (let attempt = 0; attempt < n; attempt++) {
11946
+ let res;
11947
+ try {
11948
+ res = await this.transport.request(this.hostname, attempt, this.hostname, "/api/v1/sandboxes", {
11949
+ method: "POST",
11950
+ headers: this.authHeaders(opts.idempotencyKey),
11951
+ body: JSON.stringify(params)
11952
+ });
11953
+ } catch (error) {
11954
+ lastError = error;
11955
+ continue;
11956
+ }
11957
+ if (res.status >= 400) {
11958
+ const error = RunnerError.fromResponse(res.status, res.body);
11959
+ if (error.isUnsupportedRequest) {
11960
+ return this.createViaFallback(params, opts);
11961
+ }
11962
+ if (error.isBusy) {
11963
+ lastError = error;
11964
+ continue;
11965
+ }
11966
+ throw error;
11967
+ }
11968
+ const data = res.body ? JSON.parse(res.body) : {};
11969
+ const runnerUrl = this.runnerUrlFrom(res, data);
11970
+ const id = typeof data["id"] === "string" ? data["id"] : void 0;
11971
+ if (runnerUrl !== void 0) {
11972
+ this.addressIndexByTag.set(tagFromUrl(runnerUrl), attempt);
11973
+ }
11974
+ return { id: id ?? "", runnerUrl: runnerUrl ?? "", data };
11975
+ }
11976
+ throw lastError instanceof Error ? lastError : new Error(`RunnerClient: createSandbox exhausted every address for ${this.hostname}`);
11977
+ }
11978
+ async exec(sandboxId, command, opts = {}) {
11979
+ return this.requestToSandbox(sandboxId, "POST", `/api/v1/sandboxes/${sandboxId}/exec`, {
11980
+ command,
11981
+ ...opts
11982
+ });
11983
+ }
11984
+ /** POST /api/v1/sandboxes/{id}/exec/stream - server-sent events (C2). */
11985
+ async *execStream(sandboxId, command, opts = {}) {
11986
+ const streamRes = await this.sendToSandboxStream(
11987
+ sandboxId,
11988
+ "POST",
11989
+ `/api/v1/sandboxes/${sandboxId}/exec/stream`,
11990
+ { command, ...opts }
11991
+ );
11992
+ if (streamRes.status >= 400) {
11993
+ let errorBody = "";
11994
+ for await (const chunk of streamRes.chunks) errorBody += chunk;
11995
+ throw RunnerError.fromResponse(streamRes.status, errorBody);
11996
+ }
11997
+ yield* parseSseEvents(streamRes.chunks);
11998
+ }
11999
+ /** Returns the sandbox's final usage: cpu_ms, lifetime_ms (C2). */
12000
+ async destroySandbox(sandboxId) {
12001
+ const result = await this.requestToSandbox(sandboxId, "DELETE", `/api/v1/sandboxes/${sandboxId}`);
12002
+ this.correctedTag.delete(sandboxId);
12003
+ return result;
12004
+ }
12005
+ // ─── C2b: per-sandbox routes (soma.api.v1 envelope) ──────────────────────
12006
+ async get(sandboxId, opts = {}) {
12007
+ const body5 = opts.operationId !== void 0 ? { operation_id: opts.operationId } : void 0;
12008
+ const result = await this.requestEnvelopeToSandbox(
12009
+ sandboxId,
12010
+ "GET",
12011
+ `/api/v1/sandboxes/${sandboxId}`,
12012
+ body5
12013
+ );
12014
+ return { ...result, instance_id: hyphenateInstanceId(result.instance_id) };
12015
+ }
12016
+ async stop(sandboxId, opts = {}) {
12017
+ const body5 = opts.operationId !== void 0 ? { operation_id: opts.operationId } : void 0;
12018
+ const result = await this.requestEnvelopeToSandbox(
12019
+ sandboxId,
12020
+ "POST",
12021
+ `/api/v1/sandboxes/${sandboxId}/stop`,
12022
+ body5
12023
+ );
12024
+ return { ...result, instance_id: hyphenateInstanceId(result.instance_id) };
12025
+ }
12026
+ /**
12027
+ * PATCH /api/v1/sandboxes/{id} {"timeout":seconds} - resets the idle
12028
+ * timeout (default 300s; every call to the sandbox already resets it).
12029
+ * Flat C2 response shape (C2b line 84), not the envelope - errors still
12030
+ * use the platform shape (400 INVALID_PARAM, 404 NOT_FOUND), which
12031
+ * RunnerError.fromResponse already parses either way.
12032
+ */
12033
+ async setTimeout(sandboxId, seconds) {
12034
+ const result = await this.requestToSandbox(sandboxId, "PATCH", `/api/v1/sandboxes/${sandboxId}`, {
12035
+ timeout: seconds
12036
+ });
12037
+ return result;
12038
+ }
12039
+ /**
12040
+ * GET /api/v1/sandboxes - scoped to ONE runner host (C2b: "the calling
12041
+ * tenant's sandboxes on this runner"), not the whole region. Defaults
12042
+ * to address 0; pass `tag` to target the host currently known for that
12043
+ * tag, or `addressIndex` directly.
12044
+ */
12045
+ async list(opts = {}) {
12046
+ const addressIndex = opts.tag !== void 0 ? await this.resolveAddressIndexForTag(opts.tag) : opts.addressIndex ?? 0;
12047
+ const res = await this.transport.request(this.hostname, addressIndex, this.hostname, "/api/v1/sandboxes", {
12048
+ method: "GET",
12049
+ headers: this.authHeaders()
12050
+ });
12051
+ if (res.status >= 400) throw RunnerError.fromResponse(res.status, res.body);
12052
+ const result = unwrapSomaEnvelope(res.status, res.body);
12053
+ return {
12054
+ ...result,
12055
+ sandboxes: result.sandboxes.map((entry) => ({ ...entry, instance_id: hyphenateInstanceId(entry.instance_id) }))
12056
+ };
12057
+ }
12058
+ filesystem = {
12059
+ read: (sandboxId, path) => this.filesystemOp(sandboxId, "read", { path }),
12060
+ write: (sandboxId, path, content) => this.filesystemOp(sandboxId, "write", { path, content: encodeSomaBytes(content) }),
12061
+ list: (sandboxId, path) => this.filesystemOp(sandboxId, "list", { path }),
12062
+ exists: (sandboxId, path) => this.filesystemOp(sandboxId, "exists", { path }),
12063
+ remove: (sandboxId, path, recursive) => this.filesystemOp(sandboxId, "remove", { path, ...recursive !== void 0 ? { recursive } : {} }),
12064
+ mkdir: (sandboxId, path) => this.filesystemOp(sandboxId, "mkdir", { path }),
12065
+ /** Decodes a read() result's base64 `content` into bytes. */
12066
+ decode: decodeSomaBytes
12067
+ };
12068
+ terminal = {
12069
+ open: (sandboxId, columns, rows) => this.terminalOp(sandboxId, "open", { columns, rows }),
12070
+ resize: (sandboxId, columns, rows) => this.terminalOp(sandboxId, "resize", { columns, rows }),
12071
+ write: (sandboxId, input) => this.terminalOp(sandboxId, "write", { input: encodeSomaBytes(input) }),
12072
+ read: (sandboxId, waitMs) => this.terminalOp(sandboxId, "read", waitMs !== void 0 ? { wait_ms: waitMs } : {}),
12073
+ close: (sandboxId) => this.terminalOp(sandboxId, "close", {}),
12074
+ /** Decodes a read() result's base64 `output` into bytes. */
12075
+ decode: decodeSomaBytes
12076
+ };
12077
+ /**
12078
+ * Low-level escape hatch: sends method/path straight to sandboxId's
12079
+ * runner host, with the same tag-routing, 421-follow and auth every
12080
+ * named method above uses, parsing the response as the flat C2 shape
12081
+ * (not the C2b envelope - use filesystem/terminal/get/stop/list, or
12082
+ * requestEnvelope() for an un-wrapped route not named here yet).
12083
+ */
12084
+ request(sandboxId, method, path, body5) {
12085
+ return this.requestToSandbox(sandboxId, method, path, body5);
12086
+ }
12087
+ /** Same as request(), but unwraps the response as a C2b soma.api.v1 envelope. */
12088
+ requestEnvelope(sandboxId, method, path, body5) {
12089
+ return this.requestEnvelopeToSandbox(sandboxId, method, path, body5);
12090
+ }
12091
+ async close() {
12092
+ await this.transport.close();
12093
+ }
12094
+ filesystemOp(sandboxId, op, body5) {
12095
+ return this.requestEnvelopeToSandbox(sandboxId, "POST", `/api/v1/sandboxes/${sandboxId}/filesystem/${op}`, body5);
12096
+ }
12097
+ terminalOp(sandboxId, op, body5) {
12098
+ return this.requestEnvelopeToSandbox(sandboxId, "POST", `/api/v1/sandboxes/${sandboxId}/terminal/${op}`, body5);
12099
+ }
12100
+ async createViaFallback(params, opts) {
12101
+ if (!this.fallbackCreate) {
12102
+ throw new Error(
12103
+ "RunnerClient: this create isn't served by the runner (C2) and no fallbackCreate was configured - wire RunnerClientOptions.fallbackCreate to the control-plane create path (api.miosa.ai)"
12104
+ );
12105
+ }
12106
+ return this.fallbackCreate(params, opts);
12107
+ }
12108
+ /**
12109
+ * Learns which resolved region address answers for which tag, via each
12110
+ * one's own unauthenticated `/healthz` (C2) - best effort, never awaited
12111
+ * by the constructor. A tag missed here (a host that joined after this
12112
+ * client started, or a transient `/healthz` failure) just costs a
12113
+ * repeat of this same probe on that sandbox's first real request,
12114
+ * instead of none at all.
12115
+ */
12116
+ async discoverTags() {
12117
+ try {
12118
+ const n = await this.transport.addressCount(this.hostname);
12119
+ const probes = await Promise.allSettled(
12120
+ Array.from({ length: n }, (_, i) => this.healthzByIndex(i))
12121
+ );
12122
+ probes.forEach((probe, addressIndex) => {
12123
+ if (probe.status === "fulfilled" && probe.value.tag) {
12124
+ this.addressIndexByTag.set(probe.value.tag, addressIndex);
12125
+ }
12126
+ });
12127
+ } catch {
12128
+ }
12129
+ }
12130
+ /**
12131
+ * Sends method/path to sandboxId's runner host - the id-derived tag, or
12132
+ * a previously 421-corrected one - following a single `421 misdirected`
12133
+ * to the `runner_url` it names (C6). A connect failure here is not
12134
+ * retried across other hosts: a sandbox has exactly one owner. Returns
12135
+ * the raw response; requestToSandbox/requestEnvelopeToSandbox each
12136
+ * parse it according to their route's response shape (C2 vs C2b).
12137
+ */
12138
+ async sendToSandbox(sandboxId, method, path, body5) {
12139
+ let tag = this.correctedTag.get(sandboxId) ?? sandboxTag(sandboxId);
12140
+ let addressIndex = await this.resolveAddressIndexForTag(tag);
12141
+ let followed = false;
12142
+ for (; ; ) {
12143
+ const authority = tagHostname(this.region, tag, this.baseDomain);
12144
+ const res = await this.transport.request(this.hostname, addressIndex, authority, path, {
12145
+ method,
12146
+ headers: this.authHeaders(),
12147
+ ...body5 !== void 0 ? { body: JSON.stringify(body5) } : {}
12148
+ });
12149
+ if (res.status === 421 && !followed) {
12150
+ const misdirected = RunnerError.fromResponse(res.status, res.body);
12151
+ if (misdirected.runnerUrl) {
12152
+ tag = tagFromUrl(misdirected.runnerUrl);
12153
+ addressIndex = await this.resolveAddressIndexForTag(tag);
12154
+ followed = true;
12155
+ continue;
12156
+ }
12157
+ }
12158
+ if (followed) this.correctedTag.set(sandboxId, tag);
12159
+ return res;
12160
+ }
12161
+ }
12162
+ /** Same routing as sendToSandbox, for a streaming (SSE) response. */
12163
+ async sendToSandboxStream(sandboxId, method, path, body5) {
12164
+ let tag = this.correctedTag.get(sandboxId) ?? sandboxTag(sandboxId);
12165
+ let addressIndex = await this.resolveAddressIndexForTag(tag);
12166
+ let followed = false;
12167
+ for (; ; ) {
12168
+ const authority = tagHostname(this.region, tag, this.baseDomain);
12169
+ const streamRes = await this.transport.streamRequest(this.hostname, addressIndex, authority, path, {
12170
+ method,
12171
+ headers: this.authHeaders(),
12172
+ ...body5 !== void 0 ? { body: JSON.stringify(body5) } : {}
12173
+ });
12174
+ if (streamRes.status === 421 && !followed) {
12175
+ let errorBody = "";
12176
+ for await (const chunk of streamRes.chunks) errorBody += chunk;
12177
+ const misdirected = RunnerError.fromResponse(streamRes.status, errorBody);
12178
+ if (misdirected.runnerUrl) {
12179
+ tag = tagFromUrl(misdirected.runnerUrl);
12180
+ addressIndex = await this.resolveAddressIndexForTag(tag);
12181
+ followed = true;
12182
+ continue;
12183
+ }
12184
+ if (followed) this.correctedTag.set(sandboxId, tag);
12185
+ return { status: streamRes.status, headers: streamRes.headers, chunks: singleChunk2(errorBody) };
12186
+ }
12187
+ if (followed) this.correctedTag.set(sandboxId, tag);
12188
+ return streamRes;
12189
+ }
12190
+ }
12191
+ /** C2's flat shape: parses >=400 with RunnerError, else JSON.parses the raw body. */
12192
+ async requestToSandbox(sandboxId, method, path, body5) {
12193
+ const res = await this.sendToSandbox(sandboxId, method, path, body5);
12194
+ if (res.status >= 400) throw RunnerError.fromResponse(res.status, res.body);
12195
+ return res.body ? JSON.parse(res.body) : {};
12196
+ }
12197
+ /**
12198
+ * C2b's envelope shape: a >=400 response may still be the flat C2 shape
12199
+ * (401/403/429/503/421 happen before the envelope layer) - RunnerError
12200
+ * .fromResponse parses either, so it's used directly rather than
12201
+ * routing through the envelope parser. Only a <400 response is
12202
+ * unwrapped as a genuine soma.api.v1 envelope.
12203
+ */
12204
+ async requestEnvelopeToSandbox(sandboxId, method, path, body5) {
12205
+ const res = await this.sendToSandbox(sandboxId, method, path, body5);
12206
+ if (res.status >= 400) throw RunnerError.fromResponse(res.status, res.body);
12207
+ return unwrapSomaEnvelope(res.status, res.body);
12208
+ }
12209
+ /**
12210
+ * `addressIndexByTag` first; if that tag hasn't been seen yet (a host
12211
+ * that joined after construction, or this is the very first request
12212
+ * racing the background discovery), refreshes discovery once and falls
12213
+ * back to address 0 as a last resort - the request itself still ends up
12214
+ * correct either way, via a 421 follow, just one round trip slower.
12215
+ */
12216
+ async resolveAddressIndexForTag(tag) {
12217
+ const known = this.addressIndexByTag.get(tag);
12218
+ if (known !== void 0) return known;
12219
+ await this.discoverTags();
12220
+ return this.addressIndexByTag.get(tag) ?? 0;
12221
+ }
12222
+ runnerUrlFrom(res, data) {
12223
+ const header = res.headers["soma-runner-url"];
12224
+ if (header) return header;
12225
+ const fromBody = data["runner_url"];
12226
+ return typeof fromBody === "string" ? fromBody : void 0;
12227
+ }
12228
+ authHeaders(idempotencyKey11) {
12229
+ const headers = {
12230
+ authorization: `Bearer ${this.apiKey}`,
12231
+ "content-type": "application/json",
12232
+ accept: "application/json"
12233
+ };
12234
+ if (idempotencyKey11) headers["idempotency-key"] = idempotencyKey11;
12235
+ return headers;
12236
+ }
12237
+ };
12238
+ function tagFromUrl(url) {
12239
+ let hostname;
12240
+ try {
12241
+ hostname = new URL(url).hostname;
12242
+ } catch {
12243
+ hostname = url;
12244
+ }
12245
+ return hostname.split(".")[0] ?? hostname;
12246
+ }
12247
+ async function* singleChunk2(body5) {
12248
+ if (body5) yield body5;
12249
+ }
12250
+ async function* parseSseEvents(chunks) {
12251
+ let buffer = "";
12252
+ for await (const chunk of chunks) {
12253
+ buffer += chunk;
12254
+ let newlineIdx;
12255
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
12256
+ const line = buffer.slice(0, newlineIdx).replace(/\r$/, "");
12257
+ buffer = buffer.slice(newlineIdx + 1);
12258
+ if (line.startsWith("data:")) {
12259
+ const raw = line.slice(5).trim();
12260
+ if (raw === "" || raw === "[DONE]") continue;
12261
+ try {
12262
+ yield JSON.parse(raw);
12263
+ } catch {
12264
+ }
12265
+ }
12266
+ }
12267
+ }
12268
+ }
12269
+
11028
12270
  // src/client.ts
11029
12271
  var DEFAULT_BASE_URL = "https://api.miosa.ai/api/v1";
11030
12272
  var DEFAULT_TIMEOUT2 = 3e4;
11031
12273
  var DEFAULT_MAX_RETRIES2 = 3;
12274
+ function runnerModeFromEnv() {
12275
+ return typeof process !== "undefined" && typeof process.env !== "undefined" && process.env["MIOSA_RUNNER_MODE"] === "1";
12276
+ }
12277
+ function runnerRegionFromEnv() {
12278
+ if (typeof process === "undefined" || typeof process.env === "undefined") return void 0;
12279
+ return process.env["MIOSA_RUNNER_REGION"];
12280
+ }
11032
12281
  var Miosa = class {
11033
12282
  /** Per-workspace user roster — list, add, update role, remove. */
11034
12283
  workspaceMembers;
@@ -11165,6 +12414,14 @@ var Miosa = class {
11165
12414
  network;
11166
12415
  /** Egress audit log — every outbound request, paginated query + tail. */
11167
12416
  audit;
12417
+ /**
12418
+ * SOMA one-hop runner path (opt-in via `config.runner.enabled` or
12419
+ * `MIOSA_RUNNER_MODE=1`) - talks straight to `run-<region>.miosa.ai` for
12420
+ * sandbox create/exec/destroy, bypassing the control-plane hop. `undefined`
12421
+ * unless enabled; every other resource above is unaffected either way.
12422
+ * See ONE-HOP-RUNNER-DESIGN-2026-10-02.md and RUNNER-CONTRACTS-2026-10-02.md.
12423
+ */
12424
+ runner;
11168
12425
  http;
11169
12426
  constructor(config) {
11170
12427
  if (config.apiKey && config.accessToken) {
@@ -11249,6 +12506,24 @@ var Miosa = class {
11249
12506
  this.secrets = new EgressSecrets(this.http);
11250
12507
  this.network = new EgressNetwork(this.http);
11251
12508
  this.audit = new EgressAudit(this.http);
12509
+ const runnerEnabled = config.runner?.enabled ?? runnerModeFromEnv();
12510
+ const runnerRegion = config.runner?.region ?? runnerRegionFromEnv();
12511
+ this.runner = runnerEnabled ? new RunnerClient({
12512
+ apiKey: credential,
12513
+ ...runnerRegion ? { region: runnerRegion } : {},
12514
+ ...config.runner?.baseDomain ? { baseDomain: config.runner.baseDomain } : {},
12515
+ ...config.tenant ? { tenant: config.tenant } : {},
12516
+ // C2: creates the runner doesn't serve (template/image/snapshot/
12517
+ // cwd/persistent/non-xs) go through the existing control-plane
12518
+ // path instead of failing outright.
12519
+ fallbackCreate: async (params, fallbackOpts) => {
12520
+ const sandbox = await this.sandboxes.create({
12521
+ ...params,
12522
+ ...fallbackOpts.idempotencyKey ? { idempotencyKey: fallbackOpts.idempotencyKey } : {}
12523
+ });
12524
+ return { id: sandbox.id, runnerUrl: "", data: sandbox.data };
12525
+ }
12526
+ }) : void 0;
11252
12527
  }
11253
12528
  };
11254
12529
 
@@ -11740,6 +13015,6 @@ var AppAuth = class {
11740
13015
  }
11741
13016
  };
11742
13017
 
11743
- export { AGENT_BUILD_KIND_SPECS, Admin, AgentDefinitions, AgentRuntimeProfiles, Analytics, ApiKeys, AppAuth, AppDocuments, AuditLog, AuthError, Benchmarks, BuilderSessions, Channels, Checkpoints, Cloud, CommandCenter, Community, Completions, Computer, ComputerAudit, ComputerAutoStop, ComputerConnectors, ComputerEnv, ComputerInbox, ComputerLogs, ComputerNetwork, ComputerOsa, ComputerPorts, ComputerSecrets, ComputerTerminal, ComputerVolumes, Computers, Connectors, Credits, CronJobs, DEFAULT_AGENT_BUILD_OUTPUT_ROOT, DEFAULT_AGENT_BUILD_PACKET_VERSION, Dashboard, Databases, DeploymentConnectors, DeploymentDomains, DeploymentReleases, DeploymentRuntimeInstances, DeploymentVersions, Deployments, Desktop, Devices, DockerDeploy, EgressAudit, EgressHostNotAllowedError, EgressNetwork, EgressSecrets, Email, EmailCampaigns, EmailInbox, EmailTemplates, Embeddings, Exec, ExternalKeys, Files, FlatCustomDomains, Forge, ForgeContractError, ForgePolicyViolationError, ForgeRepositories, ForgeStorageError, ForgeUnavailableError, Functions, HealthChecks, InstallationRequiredError, InsufficientCreditsError, Integrations, ManagedProviderBindingOnlyError, Mcp, Miosa, MiosaError, Models, NetworkError, NetworkPolicy, NotFoundError, OAuthFlow, OpenComputers, OrgInvites, OrganizationMembers, Organizations, ProjectAuth, ProjectIntegrations, ProjectNotLinkedError, ProviderDefaults, RateLimitError, Regions, RunGroups, Runs, RuntimeCapabilitiesResource, RuntimeEnv, SANDBOX_TEMPLATE, Sandbox, SandboxAudit, SandboxCommands, SandboxConnectors, SandboxEnv, SandboxEvents, SandboxFiles, SandboxNetwork, SandboxPreview, SandboxPreviews, SandboxSecrets, SandboxTags, SandboxTemplates, SandboxTerminal, Sandboxes, ScopeNotAllowedError, ScopedFs, Settings, SnapshotsStandalone, Storage, SubjectNotAllowedError, Templates, Tenant, TimeoutError, TokenRefreshFailedError, Usage, UserAuthorizationRequiredError, ValidationError, Volumes, Webhooks, WorkspaceInvites, WorkspaceMembers, createAgentBuildExecutionPacket, createAgentBuildExpectedOutputs, createAgentBuildPrompt, createBuildRunParams, getAgentBuildKindSpec, resolveAgentBuildKind, verifySignature };
13018
+ export { AGENT_BUILD_KIND_SPECS, Admin, AgentDefinitions, AgentRuntimeProfiles, Analytics, ApiKeys, AppAuth, AppDocuments, AuditLog, AuthError, Benchmarks, BuilderSessions, Channels, Checkpoints, Cloud, CommandCenter, Community, Completions, Computer, ComputerAudit, ComputerAutoStop, ComputerConnectors, ComputerEnv, ComputerInbox, ComputerLogs, ComputerNetwork, ComputerOsa, ComputerPorts, ComputerSecrets, ComputerTerminal, ComputerVolumes, Computers, Connectors, Credits, CronJobs, DEFAULT_AGENT_BUILD_OUTPUT_ROOT, DEFAULT_AGENT_BUILD_PACKET_VERSION, Dashboard, Databases, DeploymentConnectors, DeploymentDomains, DeploymentReleases, DeploymentRuntimeInstances, DeploymentVersions, Deployments, Desktop, Devices, DnsAddressResolver, DockerDeploy, EgressAudit, EgressHostNotAllowedError, EgressNetwork, EgressSecrets, Email, EmailCampaigns, EmailInbox, EmailTemplates, Embeddings, Exec, ExternalKeys, Files, FlatCustomDomains, Forge, ForgeContractError, ForgePolicyViolationError, ForgeRepositories, ForgeStorageError, ForgeUnavailableError, Functions, HealthChecks, Http2Transport, Http3Transport, InstallationRequiredError, InsufficientCreditsError, Integrations, ManagedProviderBindingOnlyError, Mcp, Miosa, MiosaError, Models, NetworkError, NetworkPolicy, NotFoundError, OAuthFlow, OpenComputers, OrgInvites, OrganizationMembers, Organizations, ProjectAuth, ProjectIntegrations, ProjectNotLinkedError, ProviderDefaults, RacingTransport, RateLimitError, Regions, RunGroups, RunnerClient, RunnerConnectError, RunnerError, RunnerTimeoutError, Runs, RuntimeCapabilitiesResource, RuntimeEnv, SANDBOX_TEMPLATE, Sandbox, SandboxAudit, SandboxCommands, SandboxConnectors, SandboxEnv, SandboxEvents, SandboxFiles, SandboxNetwork, SandboxPreview, SandboxPreviews, SandboxSecrets, SandboxTags, SandboxTemplates, SandboxTerminal, Sandboxes, ScopeNotAllowedError, ScopedFs, Settings, SnapshotsStandalone, StaticAddressResolver, Storage, SubjectNotAllowedError, Templates, Tenant, TimeoutError, TokenRefreshFailedError, Usage, UserAuthorizationRequiredError, ValidationError, Volumes, Webhooks, WorkspaceInvites, WorkspaceMembers, createAgentBuildExecutionPacket, createAgentBuildExpectedOutputs, createAgentBuildPrompt, createBestTransport, createBuildRunParams, decodeSomaBytes, detectHttp3Support, encodeSomaBytes, getAgentBuildKindSpec, hyphenateInstanceId, isConnectFailure, regionFromApiKey, regionHostname, resetHttp3DetectionCache, resolveAgentBuildKind, sandboxTag, tagHostname, unwrapSomaEnvelope, verifySignature };
11744
13019
  //# sourceMappingURL=index.js.map
11745
13020
  //# sourceMappingURL=index.js.map