@miosa/sdk 3.4.0 → 3.5.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
@@ -69,6 +69,14 @@ await sbx.pause();
69
69
  | `miosa.runtimeEnv` | Inherited tenant/workspace/project runtime environment variables |
70
70
  | `miosa.completions` | OpenAI-compatible chat completions with SSE streaming |
71
71
  | `miosa.embeddings` | OpenAI-compatible embedding vectors |
72
+ | `miosa.environments` | Machine templates: repositories, variables, secret files, credential toggles, versions |
73
+ | `miosa.snapshots` | Account-wide snapshot history, browse, download, fork, and `snapshots.named` |
74
+ | `miosa.billTo` | Who pays for new machines: bill-to organization, limits, member caps |
75
+ | `miosa.agentSettings` | Agents tab: provider credentials, harness settings, model catalog, sign-ins |
76
+ | `miosa.connections` | One read-model for model providers, apps and tool servers |
77
+ | `miosa.agentChats` | A chat's context use and compaction |
78
+ | `miosa.aiGateway` | AI gateway policies, budget, limits and request traces |
79
+ | `miosa.whoami()` | Who the presented credential is |
72
80
 
73
81
  ## Durable generated apps
74
82
 
@@ -415,6 +423,218 @@ Tenant preview-domain management is available through `miosa.tenant.previewDomai
415
423
  Tenant deployment-domain routing exists server-side; SDKs should consume
416
424
  deployment `public_url` instead of reconstructing it.
417
425
 
426
+ ## Environments
427
+
428
+ An environment is the template a new sandbox or computer inherits when it starts.
429
+ It carries repositories, variables, secret files, CLI tools, connections and four toggles that decide which of the owner's credentials the machine may use.
430
+ Every save that changes what a machine receives creates exactly one immutable version.
431
+ A machine pins the latest version when it starts and keeps it until you upgrade it.
432
+
433
+ ```ts
434
+ const env = await miosa.environments.create({
435
+ name: "prod",
436
+ variables: { STRIPE_KEY: "sk_live_..." },
437
+ repositories: [{ repo: "octocat/hello-world", setupScript: "npm ci", setupBlocking: true }],
438
+ passSandboxCredentials: false,
439
+ });
440
+
441
+ // One atomic save: at most one new version, all or nothing.
442
+ await miosa.environments.update("prod", {
443
+ variablesSet: { REGION: "us-east-1" },
444
+ variablesUnset: ["OLD_VAR"],
445
+ repositoriesUnset: ["octocat/old"],
446
+ });
447
+
448
+ const sbx = await miosa.sandboxes.create({ environment: "prod", env: { DEBUG: "1" } });
449
+ console.log(sbx.data.environment_status, sbx.data.environment_version);
450
+
451
+ // Machines move only when you say so. Secrets the new version withholds are deleted from the machine.
452
+ await miosa.environments.upgrade("prod", { machineIds: [sbx.id] });
453
+ ```
454
+
455
+ `environment`, `environmentId` and `noEnv` are accepted by `sandboxes.create`, `sandbox.resume`, `sandbox.fork`, `sandbox.snapshots.restore`, `computers.create`, `computer.start`, `snapshots.fork`, `snapshots.named.deploy` and `runs.run`.
456
+ `noEnv: true` gives the same guarantee as a `safe_for_third_parties` environment: nothing of the owner's passes to that machine, permanently.
457
+ `environments.effective(name)` shows the layers a machine would receive, with variable names but never values.
458
+ Reveal calls (`revealVariable`, `revealFile`) are audit-logged.
459
+
460
+ ## Setup scripts and commands
461
+
462
+ Pass `setupFile` when you create a sandbox or computer.
463
+ It runs once, in the background, after the machine is ready, and never delays `ready`.
464
+
465
+ ```ts
466
+ const sbx = await miosa.sandboxes.create({
467
+ setupFile: "#!/bin/bash\nnpm install || miosa-queue-prompt 'npm install failed'\n",
468
+ wait: 60, // hold the create request for up to 60 s; 201 either way, check sbx.ready
469
+ });
470
+
471
+ const status = await sbx.waitForSetup(); // done, failed, or null when there is no setup
472
+ if (status.setup_status === "failed") console.error(status.setup_error, status.setup_repos);
473
+ ```
474
+
475
+ `setupFile` is validated locally (UTF-8 text, at most 64 KiB, no NUL) before any machine exists, and a failed setup never destroys the machine.
476
+ `wait` takes seconds (2 to 120) or `true`, and is the `?wait=` query on the create request.
477
+
478
+ `sandbox.commands.execute()` and `computer.runCommand()` run a command synchronously (up to 600 seconds) with a relative `cwd`, `stdin` and the duration in the reply.
479
+
480
+ ```ts
481
+ const result = await sbx.commands.execute("bash ./setup.sh", { cwd: "my-repo", timeoutSeconds: 120 });
482
+ console.log(result.exit_code, result.duration_ms, result.timed_out);
483
+ ```
484
+
485
+ A machine that is not ready answers a retryable `SandboxStartingError` or `ComputerStartingError`.
486
+ The client already retries it using the server's `retry_after_ms`, so you only see it once the retry budget is spent.
487
+ `sbx.agent.prompt(text)` and `sbx.agent.stop()` steer the agent run attached to the machine.
488
+
489
+ ## Snapshots
490
+
491
+ `miosa.snapshots` is the account-wide view of snapshots for sandboxes and computers.
492
+ Browsing and downloading read the snapshot's own disk image, so they work while the machine is stopped or destroyed.
493
+
494
+ ```ts
495
+ const history = await miosa.snapshots.list({ resourceId: sbx.id });
496
+ const tree = await miosa.snapshots.tree(history[0]!.id, "/home/user"); // waits through the 202 warm-up
497
+ const bytes = await miosa.snapshots.download(history[0]!.id, "/home/user/notes.txt");
498
+ const copy = await miosa.snapshots.fork(history[0]!.id, { environment: "prod" });
499
+
500
+ // Named snapshots are pinned on purpose and never expire.
501
+ await miosa.snapshots.named.create({ name: "web-stack", sandboxId: sbx.id });
502
+ const { allowance } = await miosa.snapshots.named.list(); // 10 free names per organization
503
+ await miosa.snapshots.named.deploy("web-stack");
504
+ ```
505
+
506
+ Deleting is always explicit: `snapshots.delete(id)`, `snapshots.deleteMany(ids)` and `snapshots.deleteHistory(machineId)`.
507
+ A snapshot with dependents answers `SnapshotInUseError`, which lists them in `.dependents`.
508
+ Saving a name past the free allowance without credit answers `CreditRequiredError`.
509
+
510
+ ## Who pays: bill-to, limits and member caps
511
+
512
+ A sandbox or computer bills the organization it is created in for its whole life.
513
+
514
+ ```ts
515
+ await miosa.sandboxes.create({ billTo: "acme" }); // sent as X-Miosa-Bill-To
516
+ await miosa.billTo.set("acme"); // account-wide, for sessions and personal keys
517
+ const limits = await miosa.billTo.limits();
518
+ if (!limits.can_start) console.log(limits.blocked_reasons);
519
+ await miosa.billTo.setMemberCaps(orgId, userId, { usageCapCents: 500, maxConcurrentSandboxes: 2 });
520
+ ```
521
+
522
+ ## Agents tab: credentials, harnesses, models, sign-ins
523
+
524
+ ```ts
525
+ const settings = await miosa.agentSettings.get();
526
+ await miosa.agentSettings.putCredential("openrouter", { fields: { api_key: "sk-or-..." }, usableBy: ["agents"] });
527
+ await miosa.agentSettings.updateHarness("pi", { enabledProviders: ["anthropic", "openrouter"], defaultEffort: "medium" });
528
+ const { models } = await miosa.agentSettings.models({ harness: "claude-code" });
529
+
530
+ // Subscription sign-ins, including Kimi Code and Mistral Vibe.
531
+ const session = await miosa.agentSettings.signins.start({ provider: "kimi_code" });
532
+ console.log(session.user_code, session.verification_url);
533
+ await miosa.agentSettings.signins.waitForCompletion(session.id);
534
+
535
+ const { data } = await miosa.connections.list({ family: "models" });
536
+ ```
537
+
538
+ Agents are bring-your-own-key.
539
+ A harness with nothing connected answers `OwnCredentialsRequiredError` instead of falling back to a MIOSA key.
540
+
541
+ ## Chats, context and usage
542
+
543
+ ```ts
544
+ const run = await miosa.runs.run({ runner: "claude-code", instruction: "...", chatId, targetKind: "sandbox", targetId });
545
+ await miosa.runs.run({ runner: "claude-code", instruction: "and then?", chatId, targetKind: "sandbox", targetId }); // same session
546
+
547
+ const ctx = await miosa.agentChats.context(chatId);
548
+ if (ctx.compactable && (ctx.context?.used_ratio ?? 0) > 0.8) await miosa.agentChats.compact(chatId, { focus: "keep the plan" });
549
+
550
+ const usage = await miosa.runs.usage({ groupBy: ["agent", "day"], from: "2026-10-01" });
551
+ ```
552
+
553
+ ## SSH and port tunnels
554
+
555
+ ```ts
556
+ const info = await sbx.ssh.info();
557
+ const cert = await sbx.ssh.createCertificate({ publicKey, ttlSeconds: 900 }); // nothing stays authorized afterwards
558
+
559
+ // Needs the optional "ws" package. Binary frames, Bearer auth and the miosa-tunnel-v1 subprotocol are handled for you.
560
+ const tunnel = await sbx.tunnel.open(3000);
561
+ await tunnel.send(new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"));
562
+ for await (const chunk of tunnel) process.stdout.write(chunk);
563
+
564
+ // Node only, like `miosa proxy`.
565
+ const forward = await sbx.tunnel.forward(3000, 8080);
566
+ ```
567
+
568
+ ### One call to a working `ssh` (Node)
569
+
570
+ `ssh.connect()` makes an ed25519 key, gets a short-lived certificate for this sandbox, writes both with private modes (no `ssh-keygen` needed), starts a local relay and hands back the command.
571
+
572
+ ```ts
573
+ import { spawn } from "node:child_process";
574
+
575
+ const ssh = await sbx.ssh.connect({ ttlSeconds: 900 });
576
+ console.log(ssh.commandLine); // ssh -i ... -o CertificateFile=... -p 54321 root@127.0.0.1
577
+ console.log(ssh.config); // a Host block for ~/.ssh/config
578
+ const child = spawn(ssh.command[0]!, ssh.command.slice(1), { stdio: "inherit" });
579
+ child.on("exit", () => void ssh.close()); // stops the relay and removes the temp dir
580
+ ```
581
+
582
+ Pass `privateKeyPath` to use your own key, `dir` to keep the files, and `certificateRequest` for fields the platform adds to the certificate request.
583
+ Call `ssh.renew()` before `ssh.validBefore` to get a fresh certificate for the same key.
584
+
585
+ A tunnel that the server refuses closes with a code from the contract; the SDK raises `TunnelClosedError` with `.closeCode` and a stable `.code` such as `TUNNEL_SANDBOX_NOT_RUNNING`.
586
+
587
+ ## Identity, API key presets and the AI gateway
588
+
589
+ ```ts
590
+ const me = await miosa.whoami(); // user, organization, workspace, plan, scopes
591
+ const presets = await miosa.apiKeys.presets(); // read-only, ci, agent, cli
592
+ await miosa.apiKeys.create({ name: "ci", preset: "ci" }); // preset resolved to scopes for you
593
+
594
+ // Every create option. `expiresAt` is sent as `expires_in_days` (the server takes a lifetime, not a date).
595
+ const key = await miosa.apiKeys.create({
596
+ name: "partner-a",
597
+ scopes: ["sandboxes:read", "sandboxes:exec"],
598
+ expiresAt: new Date(Date.now() + 30 * 86_400_000),
599
+ allowedIps: ["203.0.113.0/24"],
600
+ rateLimitRpm: 600,
601
+ workspaceId: "ws_123",
602
+ });
603
+ await miosa.apiKeys.rotate(key.id, { name: "partner-a-2", overlapSeconds: 300 });
604
+
605
+ await miosa.aiGateway.createPolicy({ name: "default", primaryModel: "gpt-6-astra", fallbackModels: ["claude-sonnet-5-5"] });
606
+ const traces = await miosa.aiGateway.listRequests({ status: "error", limit: 50 });
607
+ ```
608
+
609
+ OpenComputers `jobs.start()` runs a command and returns the job at once (202); follow it with `jobs.get()` and `jobs.stream()`.
610
+
611
+ ## Files
612
+
613
+ Sandbox, managed-database and computer files go through the canonical filesystem API.
614
+
615
+ ```ts
616
+ const sbx = await miosa.sandboxes.create();
617
+ await sbx.files.write("/workspace/hello.txt", "hi");
618
+ const { entries } = await sbx.files.list("/workspace"); // name, path, is_dir, size
619
+ const tree = await sbx.files.tree("/workspace", 3); // built from the listing
620
+ const text = await sbx.files.readContent("/workspace/hello.txt"); // { content } or { content_base64 }
621
+ const bytes = await sbx.files.download("/workspace/hello.txt");
622
+ await sbx.files.mkdir("/workspace/out");
623
+ await sbx.files.rename("/workspace/hello.txt", "/workspace/out/hello.txt");
624
+ await sbx.files.uploadFile("/workspace/big.bin", blob); // multipart, for large or binary files
625
+
626
+ for await (const change of sbx.files.watch({ path: "/workspace", intervalMs: 5000 })) {
627
+ console.log(change.type, change.path); // created, modified, deleted
628
+ }
629
+
630
+ const dbFiles = miosa.databases.fs(databaseId); // same operations on /databases/:id/fs
631
+ ```
632
+
633
+ `files.watch` polls the listing: the platform's SSE watch is served only on the sandbox host, not on `api.miosa.ai`.
634
+ Each poll lists every directory down to `depth`, so keep `intervalMs` generous.
635
+ `files.writeMany` is failure-atomic only when the response says `atomicity: "failure_atomic"`; never fall back to sequential writes.
636
+ `/home/user` is the only path shared between the file API and a command run inside a Docker container.
637
+
418
638
  ## Error handling
419
639
 
420
640
  ```ts
@@ -432,6 +652,29 @@ try {
432
652
 
433
653
  The SDK retries `429` and `5xx` automatically (3 retries, exponential backoff + jitter). Set `maxRetries: 0` to disable.
434
654
  Sandbox `exec` requests are never retried, because a replayed command would run twice; see [Cancelling sandbox commands](#cancelling-sandbox-commands).
655
+ Typed subclasses exist for the codes you are likely to branch on, and each keeps the base class the status always produced (`ValidationError`, `AuthError`, `NotFoundError`, `InsufficientCreditsError`, `RateLimitError`), so existing `instanceof` checks keep matching.
656
+
657
+ | Class | Code | Retry |
658
+ |---|---|---|
659
+ | `InvalidIdError` | `INVALID_ID` (400) | no |
660
+ | `OwnCredentialsRequiredError` | `OWN_CREDENTIALS_REQUIRED` (422) | no, connect a credential |
661
+ | `EnvironmentNotFoundError` | `ENVIRONMENT_NOT_FOUND` (404 on an environment route) | no |
662
+ | `UnknownEnvironmentError` | `ENVIRONMENT_NOT_FOUND` (422 on a machine route) | no |
663
+ | `EnvironmentNameTakenError`, `EnvironmentIsDefaultError` | `ENVIRONMENT_NAME_TAKEN`, `ENVIRONMENT_IS_DEFAULT` (409) | no |
664
+ | `EnvironmentScrubFailedError` | `ENVIRONMENT_SCRUB_FAILED` (409) | yes, nothing was saved |
665
+ | `EnvironmentMemoryWithheldError` | `ENVIRONMENT_MEMORY_WITHHELD` (409, `.reasons`) | no |
666
+ | `MachineKeyScopeError` | `MACHINE_KEY_SCOPE` (403) | no |
667
+ | `SandboxStartingError`, `ComputerStartingError` | `SANDBOX_STARTING`, `COMPUTER_STARTING` (409) | yes, automatically, using `retry_after_ms` |
668
+ | `CreditRequiredError` | `CREDIT_REQUIRED` (402) | no, add credit |
669
+ | `TeamMemberCapReachedError`, `MemberLimitReachedError` | `TEAM_MEMBER_CAP_REACHED` (402), `MEMBER_LIMIT_REACHED` (429) | no |
670
+ | `BillToLockedError`, `BillToForbiddenError`, `BillToSuspendedError` | `BILL_TO_*` (403) | no |
671
+ | `OrgNotFoundError`, `AmbiguousOrgError` | `ORG_NOT_FOUND` (404), `AMBIGUOUS_ORG` (409) | no |
672
+ | `SnapshotInUseError` | `SNAPSHOT_IN_USE` (409, `.dependents`) | no |
673
+ | `RunSessionNotResumableError` | `RUN_SESSION_NOT_RESUMABLE` (409, `.reason`) | no |
674
+ | `ChatBusyError` | `CHAT_BUSY` (409) | yes, once the latest run finishes |
675
+ | `NoAgentRunError` | `NO_AGENT_RUN` (409) | no |
676
+ | `InvalidSetupFileError` | `INVALID_SETUP_FILE` (422, `.reason`) | no |
677
+
435
678
  Every `MiosaError` carries `retryable` (server-supplied when present, otherwise `true` for 408/429/5xx and transport failures) and `requestId` for support correlation.
436
679
 
437
680
  ## Interactive sandbox terminals