@decocms/sandbox-controller 0.5.0 → 0.6.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/README.md CHANGED
@@ -38,6 +38,9 @@ which are private, and bundles it into one module with declarations.
38
38
  `tenantPoolRepoSchema`), `normalizeRepoUrl`, and
39
39
  `renderTenantPoolWarmPools`, which builds a pool's `SandboxWarmPool`
40
40
  objects for the host to apply.
41
+ - Export `FreestyleSandboxProvider` (sandboxes as Freestyle VMs) and
42
+ `SandboxProviderRouter`, which serves Kubernetes and Freestyle behind one
43
+ provider; see [`packages/sandbox`](../sandbox/README.md).
41
44
  - Export the wire contract in `sandbox-api`: tool names and schemas, and the
42
45
  watch route.
43
46
  - Export host helpers: `sandboxTools` (framework-free tool definitions),
@@ -0,0 +1,37 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
13
+ var __commonJS = (cb, mod) => function __require2() {
14
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+
33
+ export {
34
+ __require,
35
+ __commonJS,
36
+ __toESM
37
+ };
package/dist/index.d.ts CHANGED
@@ -58,6 +58,8 @@ interface Sandbox {
58
58
  * expensive (a needless cold start, or minutes polling a dead port).
59
59
  */
60
60
  warmPoolAdopted: boolean;
61
+ /** Where the sandbox runs. */
62
+ provider: SandboxProviderKind;
61
63
  }
62
64
  /** When omitted, no dev server is started; the default tool image is used. */
63
65
  interface Workload {
@@ -142,7 +144,15 @@ interface EnsureRepo {
142
144
  */
143
145
  credentialExpiresAt?: number;
144
146
  }
147
+ /** Where a sandbox runs. See `SandboxProviderRouter`. */
148
+ type SandboxProviderKind = "kubernetes" | "freestyle";
145
149
  interface EnsureOptions {
150
+ /**
151
+ * Where a NEW sandbox should run, when that provider is configured and can
152
+ * serve the request. An existing sandbox stays where it is. Consumed by
153
+ * `SandboxProviderRouter`; the providers themselves ignore it.
154
+ */
155
+ provider?: SandboxProviderKind;
146
156
  /**
147
157
  * Defaults to `interactive` when absent. AgentSandboxProvider uses this to
148
158
  * decide the SandboxTemplate (memory ceiling) and the warm pool, so it has
@@ -455,7 +465,6 @@ declare const sandboxIdSchema: z.ZodObject<{
455
465
  userId: z.ZodString;
456
466
  projectRef: z.ZodString;
457
467
  }, z.core.$strip>;
458
- /** `image` is left out: the host's template pins it. */
459
468
  declare const ensureOptionsSchema: z.ZodType<Omit<EnsureOptions, "image">>;
460
469
  declare const daemonSchema: z.ZodObject<{
461
470
  url: z.ZodURL;
@@ -477,6 +486,10 @@ declare const ensureOutputSchema: z.ZodObject<{
477
486
  workdir: z.ZodString;
478
487
  previewUrl: z.ZodNullable<z.ZodString>;
479
488
  warmPoolAdopted: z.ZodBoolean;
489
+ provider: z.ZodDefault<z.ZodEnum<{
490
+ kubernetes: "kubernetes";
491
+ freestyle: "freestyle";
492
+ }>>;
480
493
  daemon: z.ZodObject<{
481
494
  url: z.ZodURL;
482
495
  token: z.ZodString;
@@ -497,6 +510,10 @@ declare const statusOutputSchema: z.ZodObject<{
497
510
  token: z.ZodString;
498
511
  }, z.core.$strip>>;
499
512
  lastTermination: z.ZodNullable<z.ZodType<PodTermination, unknown, z.core.$ZodTypeInternals<PodTermination, unknown>>>;
513
+ provider: z.ZodDefault<z.ZodEnum<{
514
+ kubernetes: "kubernetes";
515
+ freestyle: "freestyle";
516
+ }>>;
500
517
  }, z.core.$strip>;
501
518
  /** `graceMs` releases the sandbox after it; without it, the idle TTL is renewed. */
502
519
  declare const lifetimeInputSchema: z.ZodObject<{
@@ -522,6 +539,10 @@ declare const toolErrorSchema: z.ZodObject<{
522
539
  }>;
523
540
  error: z.ZodString;
524
541
  status: z.ZodOptional<z.ZodNumber>;
542
+ provider: z.ZodOptional<z.ZodEnum<{
543
+ kubernetes: "kubernetes";
544
+ freestyle: "freestyle";
545
+ }>>;
525
546
  }, z.core.$strip>;
526
547
  type ToolError = z.infer<typeof toolErrorSchema>;
527
548
  declare const claimPhaseSchema: z.ZodType<ClaimPhase>;
@@ -1226,6 +1247,9 @@ declare class AgentSandboxProvider {
1226
1247
  close(): void;
1227
1248
  }
1228
1249
 
1250
+ /** What Studio calls, served in-process or by `RemoteSandboxProvider`. */
1251
+ type SandboxProvider = Pick<AgentSandboxProvider, "ensure" | "delete" | "lastTermination" | "alive" | "watchClaimLifecycle" | "hasSchedulableCapacity" | "getPreviewUrl" | "proxyDaemonRequest" | "adoptLiveClaim" | "resolvePreviewUpstreamUrl" | "proxyPreviewRequest" | "markTenantPoolsDirty" | "releaseAfter" | "renewTtl" | "close">;
1252
+
1229
1253
  interface PostgresRunnerStateStoreOptions {
1230
1254
  url: string;
1231
1255
  /** Keeps one host's rows apart from another's sharing the database, e.g. one per Studio env. Default `""`. */
@@ -1286,6 +1310,153 @@ interface TenantPoolReader {
1286
1310
  /** A cached pool list for `AgentSandboxProvider`'s `tenantPools`; a failed load keeps the last one. */
1287
1311
  declare function tenantPoolReader(store: Pick<TenantPoolStore, "list">, cluster: string, { refreshMs }?: TenantPoolReaderOptions): TenantPoolReader;
1288
1312
 
1313
+ interface DaemonAddress {
1314
+ url: string;
1315
+ token: string;
1316
+ }
1317
+
1318
+ /**
1319
+ * `SandboxProvider` on Freestyle VMs (https://freestyle.sh). Each sandbox is
1320
+ * one VM running the same `studio-sandbox-go` image a pod runs, under Docker,
1321
+ * with the daemon's port published at `https://<handle>.<domainSuffix>`.
1322
+ *
1323
+ * The VM is the whole state: its slug is the handle and its metadata holds the
1324
+ * daemon bearer, so any Studio replica can find a sandbox without a database.
1325
+ * An idle VM is paused, not deleted, and traffic to its domain resumes it.
1326
+ */
1327
+
1328
+ interface FreestyleSandboxProviderOptions {
1329
+ apiKey: string;
1330
+ /** The sandbox image. Default: the release built from this package's version. */
1331
+ image?: string;
1332
+ /** Pause a VM after this long without network activity. Default 15 min. */
1333
+ idleTimeoutSeconds?: number;
1334
+ /** Where daemon ports are published. Default `style.dev`, free on every account. */
1335
+ domainSuffix?: string;
1336
+ /** Records daemon request latency; see `daemonProxyTimer`. */
1337
+ meter?: Meter;
1338
+ }
1339
+ declare class FreestyleSandboxProvider implements SandboxProvider {
1340
+ private readonly freestyle;
1341
+ private readonly image;
1342
+ private readonly idleTimeoutSeconds;
1343
+ private readonly domainSuffix;
1344
+ private readonly timeDaemonRequest;
1345
+ private readonly inflight;
1346
+ private readonly snapshotInflight;
1347
+ private readonly lookups;
1348
+ private readonly releaseTimers;
1349
+ constructor(opts: FreestyleSandboxProviderOptions);
1350
+ /** Whether this handle is a Freestyle sandbox. Cached; see `MISS_TTL_MS`. */
1351
+ owns(handle: string): Promise<boolean>;
1352
+ private daemonUrl;
1353
+ private find;
1354
+ private remember;
1355
+ /**
1356
+ * A snapshot of a VM with the image already pulled, so a sandbox boots in
1357
+ * seconds instead of pulling ~2GB. One per image; built on first use.
1358
+ */
1359
+ private baseSnapshot;
1360
+ private snapshotExists;
1361
+ private run;
1362
+ ensure(id: SandboxId, opts?: EnsureOptions): Promise<Sandbox>;
1363
+ private resume;
1364
+ private create;
1365
+ private configPayload;
1366
+ private waitForDaemon;
1367
+ delete(handle: string): Promise<void>;
1368
+ lastTermination(_handle: string): Promise<PodTermination | null>;
1369
+ /** A paused VM is alive: the next request resumes it. */
1370
+ alive(handle: string): Promise<boolean>;
1371
+ /** `ensure` only returns once the daemon answers, so this only has to see that. */
1372
+ watchClaimLifecycle(handle: string, signal?: AbortSignal): AsyncGenerator<ClaimPhase, void, unknown>;
1373
+ hasSchedulableCapacity(): Promise<boolean>;
1374
+ getPreviewUrl(handle: string): Promise<string | null>;
1375
+ proxyDaemonRequest(handle: string, path: string, init: ProxyRequestInit): Promise<Response>;
1376
+ /** The daemon's public address and bearer, for a host serving `sandbox-api`. */
1377
+ daemonEndpoint(handle: string): Promise<DaemonAddress | null>;
1378
+ adoptLiveClaim(_id: SandboxId, handle: string): Promise<boolean>;
1379
+ resolvePreviewUpstreamUrl(handle: string): Promise<string | null>;
1380
+ proxyPreviewRequest(handle: string, request: Request): Promise<Response>;
1381
+ markTenantPoolsDirty(_repoUrl: string, _ref: string): Promise<string[]>;
1382
+ /**
1383
+ * Pause after `graceMs`. Pausing keeps memory and disk, so this costs the
1384
+ * sandbox nothing but a resume.
1385
+ *
1386
+ * ponytail: in-process timer, lost on restart; the idle timeout pauses the
1387
+ * VM anyway, only later.
1388
+ */
1389
+ releaseAfter(handle: string, graceMs: number): Promise<void>;
1390
+ /** The VM's idle timeout already counts every request as activity. */
1391
+ renewTtl(handle: string): Promise<void>;
1392
+ private cancelRelease;
1393
+ close(): void;
1394
+ }
1395
+
1396
+ /**
1397
+ * One `SandboxProvider` over the configured providers. A new sandbox goes
1398
+ * where `EnsureOptions.provider` asks, when that provider is configured and
1399
+ * can run it; else `freestyleShare` of new sandboxes go to Freestyle, and the
1400
+ * rest to Kubernetes while it has room. An existing one stays where it is.
1401
+ * Handle-only calls go to the provider that owns the handle, because a handle
1402
+ * carries no provider tag.
1403
+ */
1404
+
1405
+ /** What a host serving `sandbox-api` needs beyond `SandboxProvider`. */
1406
+ type HostMethods = Pick<AgentSandboxProvider, "daemonEndpoint" | "listSandboxes" | "listTenantPools">;
1407
+ /** A provider that can answer whether a handle is its own without a claim. */
1408
+ type OwningSandboxProvider = SandboxProvider & Partial<Pick<HostMethods, "daemonEndpoint">> & {
1409
+ owns(handle: string): Promise<boolean>;
1410
+ };
1411
+ interface SandboxProviders {
1412
+ kubernetes?: SandboxProvider & Partial<HostMethods>;
1413
+ freestyle?: OwningSandboxProvider;
1414
+ }
1415
+ interface SandboxProviderRouterOptions {
1416
+ /**
1417
+ * Share of new sandboxes, 0 to 1, placed on Freestyle when the caller names
1418
+ * no provider. Decided by the handle, so a retry lands where the first try
1419
+ * did. Default 0.
1420
+ */
1421
+ freestyleShare?: number;
1422
+ }
1423
+ declare class SandboxProviderRouter implements SandboxProvider {
1424
+ readonly providers: SandboxProviders;
1425
+ private readonly picked;
1426
+ private readonly freestyleShare;
1427
+ constructor(providers: SandboxProviders, opts?: SandboxProviderRouterOptions);
1428
+ private get;
1429
+ private remember;
1430
+ /** Who serves an existing handle. Kubernetes is the fallback: it has no cheap ownership check. */
1431
+ providerOf(handle: string): Promise<SandboxProviderKind>;
1432
+ private owner;
1433
+ /** Where `ensure` puts this sandbox. */
1434
+ place(handle: string, opts: EnsureOptions): Promise<SandboxProviderKind>;
1435
+ ensure(id: SandboxId, opts?: EnsureOptions): Promise<Sandbox>;
1436
+ delete(handle: string): Promise<void>;
1437
+ lastTermination(handle: string): Promise<PodTermination | null>;
1438
+ alive(handle: string): Promise<boolean>;
1439
+ watchClaimLifecycle(handle: string, signal?: AbortSignal): AsyncGenerator<ClaimPhase, void, unknown>;
1440
+ /** Room anywhere: an ensure without room on Kubernetes lands on Freestyle. */
1441
+ hasSchedulableCapacity(): Promise<boolean>;
1442
+ getPreviewUrl(handle: string): Promise<string | null>;
1443
+ proxyDaemonRequest(handle: string, path: string, init: ProxyRequestInit): Promise<Response>;
1444
+ adoptLiveClaim(id: SandboxId, handle: string): Promise<boolean>;
1445
+ resolvePreviewUpstreamUrl(handle: string): Promise<string | null>;
1446
+ proxyPreviewRequest(handle: string, request: Request): Promise<Response>;
1447
+ markTenantPoolsDirty(repoUrl: string, ref: string): Promise<string[]>;
1448
+ releaseAfter(handle: string, graceMs: number): Promise<void>;
1449
+ renewTtl(handle: string): Promise<void>;
1450
+ daemonEndpoint(handle: string): Promise<{
1451
+ url: string;
1452
+ token: string;
1453
+ } | null>;
1454
+ /** Only Kubernetes sandboxes take pushed credentials. */
1455
+ listSandboxes(): ReturnType<HostMethods["listSandboxes"]>;
1456
+ listTenantPools(): ReturnType<HostMethods["listTenantPools"]>;
1457
+ close(): void;
1458
+ }
1459
+
1289
1460
  /**
1290
1461
  * The credentials a host outside Studio holds for its sandboxes. The host
1291
1462
  * never calls Studio: each `SANDBOX_ENSURE` brings the ones it was minted
@@ -1360,7 +1531,10 @@ interface SandboxToolDefinition<I extends z.ZodType = z.ZodType, O extends z.Zod
1360
1531
  outputSchema: O;
1361
1532
  execute(input: z.infer<I>): Promise<z.infer<O>>;
1362
1533
  }
1363
- type Provider = Pick<AgentSandboxProvider, "ensure" | "delete" | "alive" | "getPreviewUrl" | "lastTermination" | "daemonEndpoint" | "renewTtl" | "releaseAfter" | "hasSchedulableCapacity" | "markTenantPoolsDirty" | "listSandboxes" | "listTenantPools">;
1534
+ type Provider = {
1535
+ /** Absent on a Kubernetes-only host. */
1536
+ providerOf?(handle: string): Promise<SandboxProviderKind>;
1537
+ } & Pick<AgentSandboxProvider, "ensure" | "delete" | "alive" | "getPreviewUrl" | "lastTermination" | "daemonEndpoint" | "renewTtl" | "releaseAfter" | "hasSchedulableCapacity" | "markTenantPoolsDirty" | "listSandboxes" | "listTenantPools">;
1364
1538
 
1365
1539
  /**
1366
1540
  * `provider` is read per call, so a host can build it lazily. `credentials`
@@ -1381,4 +1555,4 @@ declare function sandboxWatchResponse(provider: Pick<AgentSandboxProvider, "watc
1381
1555
  */
1382
1556
  declare function kubeConfigForServer(server: string): KubeConfig;
1383
1557
 
1384
- export { AgentSandboxProvider, CREDENTIALS_PUSH_MAX, type ClaimPhase, type CredentialsPush, type Daemon, type EnsureOptions, type ListedTenantPool, type PortForwarder, type PostgresRunnerStateStore, type PostgresRunnerStateStoreOptions, type PostgresTenantPoolStore, type PostgresTenantPoolStoreOptions, PushedCredentials, type RepoIdentity, type RunnerStatePut, type RunnerStateRecord, type RunnerStateRecordWithId, type RunnerStateStore, type RunnerStateStoreOps, SANDBOX_LIST_MAX, SANDBOX_LIST_POOLS_MAX, SANDBOX_LIST_POOL_REPOS_MAX, SANDBOX_TOOLS, SANDBOX_WATCH_KEEPALIVE_MS, SANDBOX_WATCH_PATH, type Sandbox, type SandboxId, type SandboxList, type SandboxListing, type SandboxToolDefinition, type SandboxWarmPoolObject, type StoredTenantPool, type TenantPool, TenantPoolCapacityError, TenantPoolConflictError, type TenantPoolFieldsInput, type TenantPoolInput, type TenantPoolReader, type TenantPoolReaderOptions, type TenantPoolRepo, type TenantPoolRepoInput, type TenantPoolStore, type ToolError, capacityOutputSchema, claimPhaseSchema, credentialsPushInputSchema, credentialsPushOutputSchema, daemonSchema, emptySchema, ensureInputSchema, ensureOptionsSchema, ensureOutputSchema, handleInputSchema, kubeConfigForServer, lifetimeInputSchema, listOutputSchema, migrateSandboxControllerSchema, normalizeRepoUrl, postgresRunnerStateStore, postgresTenantPoolStore, pushedCredentialOptions, renderTenantPoolWarmPools, repoIdentitySchema, sandboxIdSchema, sandboxTools, sandboxWatchResponse, statusInputSchema, statusOutputSchema, tenantPoolFieldsSchema, tenantPoolReader, tenantPoolRepoSchema, tenantPoolSchema, tenantPoolsPushInputSchema, tenantPoolsPushOutputSchema, toolErrorSchema };
1558
+ export { AgentSandboxProvider, CREDENTIALS_PUSH_MAX, type ClaimPhase, type CredentialsPush, type Daemon, type EnsureOptions, FreestyleSandboxProvider, type FreestyleSandboxProviderOptions, type ListedTenantPool, type OwningSandboxProvider, type PortForwarder, type PostgresRunnerStateStore, type PostgresRunnerStateStoreOptions, type PostgresTenantPoolStore, type PostgresTenantPoolStoreOptions, PushedCredentials, type RepoIdentity, type RunnerStatePut, type RunnerStateRecord, type RunnerStateRecordWithId, type RunnerStateStore, type RunnerStateStoreOps, SANDBOX_LIST_MAX, SANDBOX_LIST_POOLS_MAX, SANDBOX_LIST_POOL_REPOS_MAX, SANDBOX_TOOLS, SANDBOX_WATCH_KEEPALIVE_MS, SANDBOX_WATCH_PATH, type Sandbox, type SandboxId, type SandboxList, type SandboxListing, type SandboxProviderKind, SandboxProviderRouter, type SandboxProviders, type SandboxToolDefinition, type SandboxWarmPoolObject, type StoredTenantPool, type TenantPool, TenantPoolCapacityError, TenantPoolConflictError, type TenantPoolFieldsInput, type TenantPoolInput, type TenantPoolReader, type TenantPoolReaderOptions, type TenantPoolRepo, type TenantPoolRepoInput, type TenantPoolStore, type ToolError, capacityOutputSchema, claimPhaseSchema, credentialsPushInputSchema, credentialsPushOutputSchema, daemonSchema, emptySchema, ensureInputSchema, ensureOptionsSchema, ensureOutputSchema, handleInputSchema, kubeConfigForServer, lifetimeInputSchema, listOutputSchema, migrateSandboxControllerSchema, normalizeRepoUrl, postgresRunnerStateStore, postgresTenantPoolStore, pushedCredentialOptions, renderTenantPoolWarmPools, repoIdentitySchema, sandboxIdSchema, sandboxTools, sandboxWatchResponse, statusInputSchema, statusOutputSchema, tenantPoolFieldsSchema, tenantPoolReader, tenantPoolRepoSchema, tenantPoolSchema, tenantPoolsPushInputSchema, tenantPoolsPushOutputSchema, toolErrorSchema };