@miosa/sdk 3.2.5 → 3.3.1
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 +121 -0
- package/dist/index.d.ts +797 -5
- package/dist/index.js +1175 -3
- package/dist/index.js.map +1 -1
- package/native/h3-runner/index.d.ts +61 -0
- package/native/h3-runner/index.js +60 -0
- package/native/h3-runner/npm/darwin-arm64/h3-runner-native.darwin-arm64.node +0 -0
- package/native/h3-runner/npm/darwin-x64/h3-runner-native.darwin-x64.node +0 -0
- package/native/h3-runner/npm/linux-arm64-gnu/h3-runner-native.linux-arm64-gnu.node +0 -0
- package/native/h3-runner/npm/linux-x64-gnu/h3-runner-native.linux-x64-gnu.node +0 -0
- package/native/h3-runner/package.json +9 -0
- package/package.json +6 -2
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]
|
|
@@ -181,7 +185,7 @@ var TokenRefreshFailedError = class extends MiosaError {
|
|
|
181
185
|
};
|
|
182
186
|
|
|
183
187
|
// src/version.ts
|
|
184
|
-
var SDK_VERSION = "3.
|
|
188
|
+
var SDK_VERSION = "3.3.1";
|
|
185
189
|
var SDK_USER_AGENT = `@miosa/sdk/${SDK_VERSION}`;
|
|
186
190
|
|
|
187
191
|
// src/http.ts
|
|
@@ -8778,7 +8782,16 @@ var SandboxFiles = class {
|
|
|
8778
8782
|
}
|
|
8779
8783
|
return response;
|
|
8780
8784
|
}
|
|
8781
|
-
/**
|
|
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
|
+
*/
|
|
8782
8795
|
async writeMany(files) {
|
|
8783
8796
|
const http = this.sandbox.http;
|
|
8784
8797
|
const payload = files.map((f) => ({
|
|
@@ -8853,12 +8866,54 @@ var SandboxSnapshots = class {
|
|
|
8853
8866
|
return this.sandbox.deleteSnapshot(snapshotId);
|
|
8854
8867
|
}
|
|
8855
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
|
+
}
|
|
8856
8910
|
var SandboxTerminal = class {
|
|
8857
8911
|
constructor(sandbox) {
|
|
8858
8912
|
this.sandbox = sandbox;
|
|
8859
8913
|
}
|
|
8860
8914
|
sandbox;
|
|
8861
8915
|
async create(params = {}) {
|
|
8916
|
+
assertTerminalOptions(params);
|
|
8862
8917
|
const body5 = Object.fromEntries(
|
|
8863
8918
|
Object.entries(params).filter(([, v]) => v !== void 0)
|
|
8864
8919
|
);
|
|
@@ -11128,10 +11183,1101 @@ var WorkspaceMembers = class {
|
|
|
11128
11183
|
}
|
|
11129
11184
|
};
|
|
11130
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
|
+
|
|
11131
12270
|
// src/client.ts
|
|
11132
12271
|
var DEFAULT_BASE_URL = "https://api.miosa.ai/api/v1";
|
|
11133
12272
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
11134
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
|
+
}
|
|
11135
12281
|
var Miosa = class {
|
|
11136
12282
|
/** Per-workspace user roster — list, add, update role, remove. */
|
|
11137
12283
|
workspaceMembers;
|
|
@@ -11268,6 +12414,14 @@ var Miosa = class {
|
|
|
11268
12414
|
network;
|
|
11269
12415
|
/** Egress audit log — every outbound request, paginated query + tail. */
|
|
11270
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;
|
|
11271
12425
|
http;
|
|
11272
12426
|
constructor(config) {
|
|
11273
12427
|
if (config.apiKey && config.accessToken) {
|
|
@@ -11352,6 +12506,24 @@ var Miosa = class {
|
|
|
11352
12506
|
this.secrets = new EgressSecrets(this.http);
|
|
11353
12507
|
this.network = new EgressNetwork(this.http);
|
|
11354
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;
|
|
11355
12527
|
}
|
|
11356
12528
|
};
|
|
11357
12529
|
|
|
@@ -11843,6 +13015,6 @@ var AppAuth = class {
|
|
|
11843
13015
|
}
|
|
11844
13016
|
};
|
|
11845
13017
|
|
|
11846
|
-
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 };
|
|
11847
13019
|
//# sourceMappingURL=index.js.map
|
|
11848
13020
|
//# sourceMappingURL=index.js.map
|