@miosa/sdk 3.4.1 → 3.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
@@ -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
 
@@ -390,6 +398,99 @@ console.log(rotated.viewer_password);
390
398
  await computer.destroy();
391
399
  ```
392
400
 
401
+ ## Computer use
402
+
403
+ Drive a computer by action name. The names are the members of Anthropic's `computer_toolset_20260801`, and every result is typed.
404
+
405
+ ```ts
406
+ const shot = await computer.act("screenshot"); // imageBase64, width, height
407
+ const at = await computer.act("cursor_position"); // x, y
408
+ await computer.act("left_click", { coordinate: [512, 384] });
409
+ await computer.act("key", { text: "ctrl+s" });
410
+
411
+ const view = await computer.look(); // screenshot and accessibility tree in one call
412
+ await computer.waitForDesktop(); // ready and answering
413
+ await computer.waitUntilIdle(); // screen stopped changing
414
+ const viewer = await computer.viewerUrl({ ttlSeconds: 3600 }); // { url, expiresAt, ... }
415
+ ```
416
+
417
+ The rest of the desktop is on `computer.desktop`: `hotkey`, `keyDown`, `keyUp`, `mouseDown`, `mouseUp`, `getClipboard`, `setClipboard`, `screenSize`, `accessibilityTree`, `environment`, `setWallpaper`, `screenshotRegion` and window calls (`windowSize`, `resizeWindow`, `moveWindow`, `maximizeWindow`, `minimizeWindow`, `closeWindow`).
418
+
419
+ `computer` and `sandbox` implement `Symbol.asyncDispose`, so `await using computer = await miosa.computers.create(...)` destroys it when the scope ends (TypeScript 5.2 or newer; Node 18.18 or newer).
420
+
421
+ ### Let Claude drive it
422
+
423
+ ```ts
424
+ import { miosaComputerToolset } from "@miosa/sdk/computer-use";
425
+
426
+ const toolset = miosaComputerToolset(computer, {
427
+ confirm: async ({ member, input }) => member !== "type" || askHuman(input), // required
428
+ });
429
+ // tools: [toolset] in anthropic.beta.messages.toolRunner(...), or toolset.toJSON() and toolset.toolResult(block) in your own loop.
430
+ ```
431
+
432
+ `@miosa/sdk/computer-use` subclasses Anthropic's `BetaAbstractComputerToolset20260801`, so the batch rules (stop at the first failure, `Not executed: an earlier computer action in this turn failed.` for the rest, `toolset_name` on every result) come from Anthropic's runner.
433
+ It needs the optional peer `@anthropic-ai/sdk` 0.133 or newer.
434
+ `confirm` is required because the toolset can type and press keys; the alternative is to switch `type`, `key` and `hold_key` off in `configs`.
435
+ `zoom` is a region screenshot.
436
+ `triple_click` is off by default because the desktop has no triple-click route; `emulateTripleClick: true` sends a click and then a double-click.
437
+ Screens are sent at their native size, so pick a 1024x768 or 1280x720 computer.
438
+ For older models use `miosaComputerToolLegacy(computer, { version, displayWidthPx, displayHeightPx, confirm })`.
439
+
440
+ See `examples/computer-use-claude.ts`.
441
+
442
+ ## End-user tokens (platform builders)
443
+
444
+ `miosa.tokens.createScoped()` is the path for a platform that serves many customers.
445
+ Your server mints a short-lived token for one end user and one workspace, and the browser holds only that token.
446
+ It carries the scopes you ask for and never more than the minting key holds.
447
+
448
+ ```ts
449
+ const { token, expires_at } = await miosa.tokens.createScoped({
450
+ userId: "end-user-123",
451
+ workspaceId: "ws_abc",
452
+ scopes: ["sandboxes:create", "sandboxes:read"],
453
+ expiresInSeconds: 3600,
454
+ });
455
+ // give `token` to the browser: new Miosa({ apiKey: token })
456
+ ```
457
+
458
+ See `sdks/docs/guides/build-a-multi-tenant-platform.md` for the whole flow.
459
+
460
+ ## Helpers
461
+
462
+ `@miosa/sdk/helpers` puts the calls an agent loop reaches for in one import, keyed by computer id.
463
+
464
+ | Helper | Does |
465
+ |---|---|
466
+ | `waitUntilReady(miosa, id)` | poll until the computer is `active` or `running` |
467
+ | `waitForDesktop(miosa, id)` | ready, and the desktop answers |
468
+ | `waitUntilIdle(miosa, id)` | the screen stopped changing |
469
+ | `waitForPrompt(miosa, runId)` | wait for an agent run to finish |
470
+ | `streamPrompt(miosa, runId)` | follow an agent run's activity |
471
+ | `streamEvents(miosa, id, { subscribe })` | follow window, file, process, clipboard and idle events |
472
+ | `streamCommand(miosa, id, command)` | run a command and follow its output |
473
+ | `execCommand(miosa, id, command)` | run a command and get the result |
474
+ | `readText` / `writeText` | UTF-8 files |
475
+ | `stopAndRemove(miosa, id)` | stop, then destroy; safe to call twice |
476
+
477
+ Timeouts raise a `MiosaError` with a stable code (`COMPUTER_READY_TIMEOUT`, `DESKTOP_READY_TIMEOUT`, `DESKTOP_IDLE_TIMEOUT`) and `retryable: true`.
478
+
479
+ ## Fail fast
480
+
481
+ Pass `failFast: true` to `sandboxes.create`, `fork`, `resume`, `computers.create` or `computer.start` to shed load instead of waiting for capacity.
482
+ When no ready machine is free the server answers `503 no_ready_machine` in about 1.5 seconds, and the SDK raises `NoReadyMachineError`.
483
+ A fail-fast request is never retried by the client, because the refusal is the point.
484
+
485
+ ```ts
486
+ try {
487
+ const sbx = await miosa.sandboxes.create({ failFast: true, size: "large" });
488
+ } catch (error) {
489
+ if (error instanceof NoReadyMachineError) return fallback(); // retryAfterMs is the server's hint
490
+ throw error;
491
+ }
492
+ ```
493
+
393
494
  ## White-label / multi-tenant
394
495
 
395
496
  ```ts
@@ -415,6 +516,218 @@ Tenant preview-domain management is available through `miosa.tenant.previewDomai
415
516
  Tenant deployment-domain routing exists server-side; SDKs should consume
416
517
  deployment `public_url` instead of reconstructing it.
417
518
 
519
+ ## Environments
520
+
521
+ An environment is the template a new sandbox or computer inherits when it starts.
522
+ It carries repositories, variables, secret files, CLI tools, connections and four toggles that decide which of the owner's credentials the machine may use.
523
+ Every save that changes what a machine receives creates exactly one immutable version.
524
+ A machine pins the latest version when it starts and keeps it until you upgrade it.
525
+
526
+ ```ts
527
+ const env = await miosa.environments.create({
528
+ name: "prod",
529
+ variables: { STRIPE_KEY: "sk_live_..." },
530
+ repositories: [{ repo: "octocat/hello-world", setupScript: "npm ci", setupBlocking: true }],
531
+ passSandboxCredentials: false,
532
+ });
533
+
534
+ // One atomic save: at most one new version, all or nothing.
535
+ await miosa.environments.update("prod", {
536
+ variablesSet: { REGION: "us-east-1" },
537
+ variablesUnset: ["OLD_VAR"],
538
+ repositoriesUnset: ["octocat/old"],
539
+ });
540
+
541
+ const sbx = await miosa.sandboxes.create({ environment: "prod", env: { DEBUG: "1" } });
542
+ console.log(sbx.data.environment_status, sbx.data.environment_version);
543
+
544
+ // Machines move only when you say so. Secrets the new version withholds are deleted from the machine.
545
+ await miosa.environments.upgrade("prod", { machineIds: [sbx.id] });
546
+ ```
547
+
548
+ `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`.
549
+ `noEnv: true` gives the same guarantee as a `safe_for_third_parties` environment: nothing of the owner's passes to that machine, permanently.
550
+ `environments.effective(name)` shows the layers a machine would receive, with variable names but never values.
551
+ Reveal calls (`revealVariable`, `revealFile`) are audit-logged.
552
+
553
+ ## Setup scripts and commands
554
+
555
+ Pass `setupFile` when you create a sandbox or computer.
556
+ It runs once, in the background, after the machine is ready, and never delays `ready`.
557
+
558
+ ```ts
559
+ const sbx = await miosa.sandboxes.create({
560
+ setupFile: "#!/bin/bash\nnpm install || miosa-queue-prompt 'npm install failed'\n",
561
+ wait: 60, // hold the create request for up to 60 s; 201 either way, check sbx.ready
562
+ });
563
+
564
+ const status = await sbx.waitForSetup(); // done, failed, or null when there is no setup
565
+ if (status.setup_status === "failed") console.error(status.setup_error, status.setup_repos);
566
+ ```
567
+
568
+ `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.
569
+ `wait` takes seconds (2 to 120) or `true`, and is the `?wait=` query on the create request.
570
+
571
+ `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.
572
+
573
+ ```ts
574
+ const result = await sbx.commands.execute("bash ./setup.sh", { cwd: "my-repo", timeoutSeconds: 120 });
575
+ console.log(result.exit_code, result.duration_ms, result.timed_out);
576
+ ```
577
+
578
+ A machine that is not ready answers a retryable `SandboxStartingError` or `ComputerStartingError`.
579
+ The client already retries it using the server's `retry_after_ms`, so you only see it once the retry budget is spent.
580
+ `sbx.agent.prompt(text)` and `sbx.agent.stop()` steer the agent run attached to the machine.
581
+
582
+ ## Snapshots
583
+
584
+ `miosa.snapshots` is the account-wide view of snapshots for sandboxes and computers.
585
+ Browsing and downloading read the snapshot's own disk image, so they work while the machine is stopped or destroyed.
586
+
587
+ ```ts
588
+ const history = await miosa.snapshots.list({ resourceId: sbx.id });
589
+ const tree = await miosa.snapshots.tree(history[0]!.id, "/home/user"); // waits through the 202 warm-up
590
+ const bytes = await miosa.snapshots.download(history[0]!.id, "/home/user/notes.txt");
591
+ const copy = await miosa.snapshots.fork(history[0]!.id, { environment: "prod" });
592
+
593
+ // Named snapshots are pinned on purpose and never expire.
594
+ await miosa.snapshots.named.create({ name: "web-stack", sandboxId: sbx.id });
595
+ const { allowance } = await miosa.snapshots.named.list(); // 10 free names per organization
596
+ await miosa.snapshots.named.deploy("web-stack");
597
+ ```
598
+
599
+ Deleting is always explicit: `snapshots.delete(id)`, `snapshots.deleteMany(ids)` and `snapshots.deleteHistory(machineId)`.
600
+ A snapshot with dependents answers `SnapshotInUseError`, which lists them in `.dependents`.
601
+ Saving a name past the free allowance without credit answers `CreditRequiredError`.
602
+
603
+ ## Who pays: bill-to, limits and member caps
604
+
605
+ A sandbox or computer bills the organization it is created in for its whole life.
606
+
607
+ ```ts
608
+ await miosa.sandboxes.create({ billTo: "acme" }); // sent as X-Miosa-Bill-To
609
+ await miosa.billTo.set("acme"); // account-wide, for sessions and personal keys
610
+ const limits = await miosa.billTo.limits();
611
+ if (!limits.can_start) console.log(limits.blocked_reasons);
612
+ await miosa.billTo.setMemberCaps(orgId, userId, { usageCapCents: 500, maxConcurrentSandboxes: 2 });
613
+ ```
614
+
615
+ ## Agents tab: credentials, harnesses, models, sign-ins
616
+
617
+ ```ts
618
+ const settings = await miosa.agentSettings.get();
619
+ await miosa.agentSettings.putCredential("openrouter", { fields: { api_key: "sk-or-..." }, usableBy: ["agents"] });
620
+ await miosa.agentSettings.updateHarness("pi", { enabledProviders: ["anthropic", "openrouter"], defaultEffort: "medium" });
621
+ const { models } = await miosa.agentSettings.models({ harness: "claude-code" });
622
+
623
+ // Subscription sign-ins, including Kimi Code and Mistral Vibe.
624
+ const session = await miosa.agentSettings.signins.start({ provider: "kimi_code" });
625
+ console.log(session.user_code, session.verification_url);
626
+ await miosa.agentSettings.signins.waitForCompletion(session.id);
627
+
628
+ const { data } = await miosa.connections.list({ family: "models" });
629
+ ```
630
+
631
+ Agents are bring-your-own-key.
632
+ A harness with nothing connected answers `OwnCredentialsRequiredError` instead of falling back to a MIOSA key.
633
+
634
+ ## Chats, context and usage
635
+
636
+ ```ts
637
+ const run = await miosa.runs.run({ runner: "claude-code", instruction: "...", chatId, targetKind: "sandbox", targetId });
638
+ await miosa.runs.run({ runner: "claude-code", instruction: "and then?", chatId, targetKind: "sandbox", targetId }); // same session
639
+
640
+ const ctx = await miosa.agentChats.context(chatId);
641
+ if (ctx.compactable && (ctx.context?.used_ratio ?? 0) > 0.8) await miosa.agentChats.compact(chatId, { focus: "keep the plan" });
642
+
643
+ const usage = await miosa.runs.usage({ groupBy: ["agent", "day"], from: "2026-10-01" });
644
+ ```
645
+
646
+ ## SSH and port tunnels
647
+
648
+ ```ts
649
+ const info = await sbx.ssh.info();
650
+ const cert = await sbx.ssh.createCertificate({ publicKey, ttlSeconds: 900 }); // nothing stays authorized afterwards
651
+
652
+ // Needs the optional "ws" package. Binary frames, Bearer auth and the miosa-tunnel-v1 subprotocol are handled for you.
653
+ const tunnel = await sbx.tunnel.open(3000);
654
+ await tunnel.send(new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n"));
655
+ for await (const chunk of tunnel) process.stdout.write(chunk);
656
+
657
+ // Node only, like `miosa proxy`.
658
+ const forward = await sbx.tunnel.forward(3000, 8080);
659
+ ```
660
+
661
+ ### One call to a working `ssh` (Node)
662
+
663
+ `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.
664
+
665
+ ```ts
666
+ import { spawn } from "node:child_process";
667
+
668
+ const ssh = await sbx.ssh.connect({ ttlSeconds: 900 });
669
+ console.log(ssh.commandLine); // ssh -i ... -o CertificateFile=... -p 54321 root@127.0.0.1
670
+ console.log(ssh.config); // a Host block for ~/.ssh/config
671
+ const child = spawn(ssh.command[0]!, ssh.command.slice(1), { stdio: "inherit" });
672
+ child.on("exit", () => void ssh.close()); // stops the relay and removes the temp dir
673
+ ```
674
+
675
+ Pass `privateKeyPath` to use your own key, `dir` to keep the files, and `certificateRequest` for fields the platform adds to the certificate request.
676
+ Call `ssh.renew()` before `ssh.validBefore` to get a fresh certificate for the same key.
677
+
678
+ 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`.
679
+
680
+ ## Identity, API key presets and the AI gateway
681
+
682
+ ```ts
683
+ const me = await miosa.whoami(); // user, organization, workspace, plan, scopes
684
+ const presets = await miosa.apiKeys.presets(); // read-only, ci, agent, cli
685
+ await miosa.apiKeys.create({ name: "ci", preset: "ci" }); // preset resolved to scopes for you
686
+
687
+ // Every create option. `expiresAt` is sent as `expires_in_days` (the server takes a lifetime, not a date).
688
+ const key = await miosa.apiKeys.create({
689
+ name: "partner-a",
690
+ scopes: ["sandboxes:read", "sandboxes:exec"],
691
+ expiresAt: new Date(Date.now() + 30 * 86_400_000),
692
+ allowedIps: ["203.0.113.0/24"],
693
+ rateLimitRpm: 600,
694
+ workspaceId: "ws_123",
695
+ });
696
+ await miosa.apiKeys.rotate(key.id, { name: "partner-a-2", overlapSeconds: 300 });
697
+
698
+ await miosa.aiGateway.createPolicy({ name: "default", primaryModel: "gpt-6-astra", fallbackModels: ["claude-sonnet-5-5"] });
699
+ const traces = await miosa.aiGateway.listRequests({ status: "error", limit: 50 });
700
+ ```
701
+
702
+ OpenComputers `jobs.start()` runs a command and returns the job at once (202); follow it with `jobs.get()` and `jobs.stream()`.
703
+
704
+ ## Files
705
+
706
+ Sandbox, managed-database and computer files go through the canonical filesystem API.
707
+
708
+ ```ts
709
+ const sbx = await miosa.sandboxes.create();
710
+ await sbx.files.write("/workspace/hello.txt", "hi");
711
+ const { entries } = await sbx.files.list("/workspace"); // name, path, is_dir, size
712
+ const tree = await sbx.files.tree("/workspace", 3); // built from the listing
713
+ const text = await sbx.files.readContent("/workspace/hello.txt"); // { content } or { content_base64 }
714
+ const bytes = await sbx.files.download("/workspace/hello.txt");
715
+ await sbx.files.mkdir("/workspace/out");
716
+ await sbx.files.rename("/workspace/hello.txt", "/workspace/out/hello.txt");
717
+ await sbx.files.uploadFile("/workspace/big.bin", blob); // multipart, for large or binary files
718
+
719
+ for await (const change of sbx.files.watch({ path: "/workspace", intervalMs: 5000 })) {
720
+ console.log(change.type, change.path); // created, modified, deleted
721
+ }
722
+
723
+ const dbFiles = miosa.databases.fs(databaseId); // same operations on /databases/:id/fs
724
+ ```
725
+
726
+ `files.watch` polls the listing: the platform's SSE watch is served only on the sandbox host, not on `api.miosa.ai`.
727
+ Each poll lists every directory down to `depth`, so keep `intervalMs` generous.
728
+ `files.writeMany` is failure-atomic only when the response says `atomicity: "failure_atomic"`; never fall back to sequential writes.
729
+ `/home/user` is the only path shared between the file API and a command run inside a Docker container.
730
+
418
731
  ## Error handling
419
732
 
420
733
  ```ts
@@ -432,6 +745,29 @@ try {
432
745
 
433
746
  The SDK retries `429` and `5xx` automatically (3 retries, exponential backoff + jitter). Set `maxRetries: 0` to disable.
434
747
  Sandbox `exec` requests are never retried, because a replayed command would run twice; see [Cancelling sandbox commands](#cancelling-sandbox-commands).
748
+ 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.
749
+
750
+ | Class | Code | Retry |
751
+ |---|---|---|
752
+ | `InvalidIdError` | `INVALID_ID` (400) | no |
753
+ | `OwnCredentialsRequiredError` | `OWN_CREDENTIALS_REQUIRED` (422) | no, connect a credential |
754
+ | `EnvironmentNotFoundError` | `ENVIRONMENT_NOT_FOUND` (404 on an environment route) | no |
755
+ | `UnknownEnvironmentError` | `ENVIRONMENT_NOT_FOUND` (422 on a machine route) | no |
756
+ | `EnvironmentNameTakenError`, `EnvironmentIsDefaultError` | `ENVIRONMENT_NAME_TAKEN`, `ENVIRONMENT_IS_DEFAULT` (409) | no |
757
+ | `EnvironmentScrubFailedError` | `ENVIRONMENT_SCRUB_FAILED` (409) | yes, nothing was saved |
758
+ | `EnvironmentMemoryWithheldError` | `ENVIRONMENT_MEMORY_WITHHELD` (409, `.reasons`) | no |
759
+ | `MachineKeyScopeError` | `MACHINE_KEY_SCOPE` (403) | no |
760
+ | `SandboxStartingError`, `ComputerStartingError` | `SANDBOX_STARTING`, `COMPUTER_STARTING` (409) | yes, automatically, using `retry_after_ms` |
761
+ | `CreditRequiredError` | `CREDIT_REQUIRED` (402) | no, add credit |
762
+ | `TeamMemberCapReachedError`, `MemberLimitReachedError` | `TEAM_MEMBER_CAP_REACHED` (402), `MEMBER_LIMIT_REACHED` (429) | no |
763
+ | `BillToLockedError`, `BillToForbiddenError`, `BillToSuspendedError` | `BILL_TO_*` (403) | no |
764
+ | `OrgNotFoundError`, `AmbiguousOrgError` | `ORG_NOT_FOUND` (404), `AMBIGUOUS_ORG` (409) | no |
765
+ | `SnapshotInUseError` | `SNAPSHOT_IN_USE` (409, `.dependents`) | no |
766
+ | `RunSessionNotResumableError` | `RUN_SESSION_NOT_RESUMABLE` (409, `.reason`) | no |
767
+ | `ChatBusyError` | `CHAT_BUSY` (409) | yes, once the latest run finishes |
768
+ | `NoAgentRunError` | `NO_AGENT_RUN` (409) | no |
769
+ | `InvalidSetupFileError` | `INVALID_SETUP_FILE` (422, `.reason`) | no |
770
+
435
771
  Every `MiosaError` carries `retryable` (server-supplied when present, otherwise `true` for 408/429/5xx and transport failures) and `requestId` for support correlation.
436
772
 
437
773
  ## Interactive sandbox terminals