@finalai/final-agent-provider 0.3.4 → 0.4.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
@@ -26,3 +26,14 @@ Adding a model on an already-supported provider API requires a platform catalog
26
26
  The `modelZod(ids)` and `modelJson(ids, defaultId)` helpers retain explicit restrictions for third-party harness authors who choose to supply them.
27
27
 
28
28
  SDK 0.2.0 removes `@finalai/agents/models` and `@finalai/final-agent-provider/harnesses/models`, including their model arrays, defaults, model-ID unions and pricing helpers. Use string model references and the installed platform’s harness description for current candidates and defaults. Pricing belongs to the hosting platform. The protocol-level `ModelVendor` enum remains available from `@finalai/final-agent-provider/harnesses`.
29
+
30
+ ## Workspace Device egress
31
+
32
+ `/sandboxes/egress` supplies provider-neutral Linux TCP/UDP/DNS routing through approved Final Devices. Hosts compose `createWorkspaceDeviceNetworking({ api, needed })` through the generic provider `networking` option. They supply stable logical workspace identity, durable networking registration, the current actor, and the raw privileged sandbox session. Preserve the direct policy before boot fencing; restore it on Disable. Configure and Disable ask for consent by default.
33
+
34
+ The [integration reference](WORKSPACE-EGRESS.md) specifies required hooks, boot/resume fallback, binding authority, teardown, MCP transports, failure behavior, prerequisites, and provider acceptance. Read it before enabling this capability. Final Computer and Daytona use their public APIs; custom providers can compose the same controller and manager.
35
+
36
+ In 0.4, passive Computer recovery follows the read-only viewer contract even without
37
+ a networking extension. Use ordinary resume for execution. The bundled adapters
38
+ cache deferred setup failures per handle; reconnect to retry rather than replaying
39
+ partially completed user setup commands on each MCP poll.
@@ -0,0 +1,172 @@
1
+ # Workspace Device egress integration
2
+
3
+ `/sandboxes/egress` provides a provider-neutral TCP/UDP/DNS exit through an approved Final Device. IPv6 application egress is blocked; kernel and root-owned neighbor discovery remain available; link neighbor discovery remains available for inbound provider control. Selecting a Device intentionally permits access to its reachable LAN and services addressed through its tailnet IP, under its owner/process permission. Grants exclude other workspaces and unapproved Devices. Accepted provider control and preview TCP replies retain their physical return path; this capability routes outbound workspace connections.
4
+
5
+ ## Host composition and identity
6
+
7
+ Final Device egress is an opinionated implementation of the generic
8
+ `SandboxNetworkingExtensionIface`, not a requirement on sandbox implementations or
9
+ a public replacement-Device protocol. Compose it with a provider through the same
10
+ `networking` option used by other networking implementations:
11
+
12
+ ```ts
13
+ const networking = createWorkspaceDeviceNetworking({ api: deviceApi, needed });
14
+ const computers = createComputerSandboxProvider({ computersApi, networking });
15
+ const daytona = createDaytonaSandboxProvider({ credential: daytonaCredential, networking });
16
+ ```
17
+
18
+ The factories are exported from `/sandboxes/egress`, `/sandboxes/computer`, and
19
+ `@finalai/daytona-agent-provider/sandboxes/daytona`, respectively. Keep the trusted
20
+ organization credential reader in host composition. A custom provider accepts
21
+ the same generic extension: call `enabled`, validate its workspace identity, call
22
+ `prepare` before provider fencing, and use `attachSandboxNetworking` after
23
+ obtaining its raw handle. Supply the saved policy, restore/fence callbacks,
24
+ namespace existence check and deferred setup. Physical teardown calls `suspend`
25
+ even if another cleanup step fails. Providers needing earlier namespace setup can
26
+ use `controller` and `withSandboxNetworking` directly, as Final Computer does.
27
+
28
+ The extension owns Device authorization, enrollment, helper installation, leases
29
+ and failover. Providers need no Device or Headscale implementation. Its generic
30
+ controller also authorizes external editor access and reports its expiry limit;
31
+ providers must bound editor grants by that deadline. The Device executable and
32
+ hosted control services remain separate first-party implementations.
33
+
34
+ The public Computers API accepts an optional browser credential `expiresAt` bound.
35
+ Managed editors request that bound and refuse a backend response that exceeds it;
36
+ an older API rejecting the field cannot silently grant longer access.
37
+
38
+ 1. Assign one stable host-owned identity per logical workspace and persist whether networking is managed. Supply that identity to start, resume, editors and teardown.
39
+ 2. Save the actual direct policy before a boot fence. Prefer a provider fence; explicitly unsupported providers use the logged startup-window fallback.
40
+ 3. Construct the manager with the raw session and privileged executor, then declare its policy on the provider handle with its binding access and current-actor reader.
41
+ 4. Detach on runtime shutdown. On physical stop/deletion, attempt fencing, lease cleanup and provider cleanup independently; retain saved desired settings for recreation.
42
+
43
+ Inject the Device API URL, trusted organization credential reader, stable workspace ID, current acting user, and the raw Linux sandbox session. Pass that unguarded session into `createWorkspaceEgressManager`, then use `withWorkspaceEgress` to declare its execution policy and lifecycle hooks on the returned handle. `withWorkspaceEgress` constructs a checked handle through the generic SDK constructor, so direct callers are safe too. `defineSandboxProvider` automatically adds any host admission policy on start and resume without reinstalling the Device policy. Using a guarded session for privileged controller execution recursively deadlocks. `/sandboxes/egress-bootstrap` exports the exact controller source for custom privileged executors and manual acceptance. Custom executors run `python3 -I -c source source` with JSON on stdin, or execute a root-owned file providing `__file__`; the source argument permits persistence for later controller launches.
44
+
45
+ The host owns the logical workspace identity and durable networking registration. Each identity must name one physical workspace at a time; do not reuse it for unrelated resources or derive authorization from opaque provider state. Editor contexts supply the same `workspaceId` as start/resume. When `networking` is configured, legacy editor states also require that host identity so the predicate can determine management before granting access. Managed editor startup is guarded. Missing enrolled workspace nodes automatically recover using the approved desired configuration.
46
+
47
+ Supply the extension's `enabled` predicate through `createWorkspaceDeviceNetworking({ needed, api })`. Configuring an API client alone does not opt in workspaces. Keep the predicate true for every workspace with saved networking policy, including an explicitly disabled allow-all route and after new opt-in is removed. Teardown can leave a provider boot fence, so management and the Device API remain required until the stable workspace is deleted. The adapters also remember management within their current process; that cannot replace durable host discovery after a restart. Hosts filter potential provider capabilities per workspace. `additionalCapabilities` offers known positive capabilities while the rest of pre-start availability stays unknown; Computer uses it for egress without claiming Screen or ComputerUse availability.
48
+
49
+ Before creating or booting a fenced namespace, call `preserveWorkspaceNetworkPolicy` with its actual current provider policy. This atomically saves only a missing policy; a temporary boot fence must never replace a saved choice. Both providers call the extension's `prepare` hook for durable create-if-absent behavior, and its `suspend` hook during physical teardown. The Final Device extension implements those hooks through the Device API. The initialization endpoint is deliberately separate: an older Device API rejects boot before changing provider policy rather than misinterpreting the operation as a policy update.
50
+
51
+ Custom networking hooks own resources allocated before they reject and must clean up those resources. A failed attachment must not revoke another binding’s shared lease. The Device API must expire provisioning leases whose create acknowledgement is lost; a client cannot revoke an ID it never received, and cannot activate it without the acknowledged enrollment.
52
+
53
+ Adapters must supply `afterGuard` to restore restrictive saved policies or undo an outer fence. Missing restoration refuses Configure/Disable rather than acknowledging an unrestricted direct route. Provider instances retain remembered managed-workspace and prepared-policy identifiers for their lifetime; include that inventory in host capacity planning. Durable host registration remains required across process replacement.
54
+
55
+ ## Boot, resume and saved policy
56
+
57
+ Prefer provider networking fences before boot or rebase, then apply the transport policy only after the manager guards the namespace. Daytona defaults to a structured-warning fallback only when the account explicitly rejects custom networking; `workspaceNetworkingBootFence: true` requires the provider fence, and `false` deliberately selects command-readiness protection. Startup/resumed background traffic before controller initialization is outside the fallback guarantee. A fresh managed read binding records the intended policy before creation, then stays fenced until a write binding recovers it. Existing running read bindings remain passive. Read bindings cannot wake a managed paused/stopped Computer or stopped/archived Daytona workspace; a write binding must resume it through guarded initialization. Pass the host’s logical `session.workspaceId` through every lifecycle call. Generic `sandboxWorkspaceId` derives conversation/target identity across replacement bindings when absent; provider resource names remain private. New session workspaces use binding-derived identities and the guide's initial policy. Missing or unreadable provider policy is denied. Fallback identities escape delimiters; Device egress identities must fit the 256-character protocol limit without restricting ordinary sandbox identities; supply a bounded host identity for longer provider names. Disable before capturing an image; do not clone a live managed identity.
58
+
59
+ Device configuration and Disable ask for approval by default through the public sandbox tool policy, and can be explicitly preauthorized. Consent selects the Device’s destination controls, including private access; provider destination allowlists do not constrain traffic inside the tunnel. Retain the saved direct-access policy and restore it on Disable. Unknown initial policy prevents configuration because restoration would be undefined. Disable before changing the saved direct-access policy. In-tunnel destination allowlists are unsupported. Configure failures intentionally retain a blocked route and desired settings until successful configuration or explicit disable; they never silently restore direct access.
60
+
61
+ Daytona treats block-all, CIDR allowlists, and domain allowlists as mutually exclusive. Its adapter explicitly clears domain allowlists on block/unblock, including legacy servers that clear only CIDRs; eligible-account acceptance must verify actual enforcement before admission, especially whether settings accepted on a stopped or archived workspace are applied during boot. Settings read-back alone does not prove that boot-time fence. See [Daytona network limits](https://www.daytona.io/docs/en/network-limits/).
62
+
63
+ Provider instance deletion does not erase saved logical-workspace networking. Recreating the same logical workspace deliberately recovers it; never reuse a workspace identifier for an unrelated workspace. A new managed attachment must preserve the initial policy through Device API even if the instance is already running.
64
+
65
+ An authorized Configure that subsequently fails carries a separate persistent choice marker, including before enrollment begins; resolver/firewall restoration artifacts do not authorize returning to direct access. The marker survives loss of volatile controller state and clears only after explicit Disable or an authorized direct-policy restoration. Custom providers may supply `namespaceExists` from their own authoritative API so deletion cleanup revokes leases without entering a deleted namespace. In-guest reboots and direct provider administration are outside the host lifecycle fence; route restarts through the adapter to retain boot-time protection.
66
+
67
+ ## Execution policy boundary
68
+
69
+ The generic handle constructor applies the handle's `executionPolicy` automatically.
70
+ The provider factory composes any host policy supplied in the start context before
71
+ that policy, including when the handle was already constructed. The Final
72
+ integration declares Device authorization, lease readiness and deferred setup;
73
+ it does not install its own execution wrapper. The generic factory does not read
74
+ the acting user, call Device API, select Devices or decide approvals.
75
+
76
+ Policy checks cover the standard commands, mutations and MCP channels. Configure
77
+ and Disable retain separate authorization so they can repair blocked networking.
78
+ The direct-network policy setter performs its own authorization and repair without
79
+ requiring a working tunnel. Host-supplied admission still applies to that setter.
80
+ A workspace shares one network namespace; checks on new operations do not isolate
81
+ already-running processes by user. The Linux firewall independently enforces
82
+ physical-egress denial.
83
+
84
+ ## Authority, bindings and shutdown
85
+
86
+ Device Configure/Disable require write access and lifecycle authority. Direct network-policy operations require execute or write access plus lifecycle authority; read bindings cannot mutate networking. MCP projections enforce tool consent; hosts exposing handle RPC must install their own operation-consent hook. Configure and Disable default to Ask. Only exact `network.egress.configure`/`network.egress.disable` action preauthorization can waive the default; broad lifecycle/network Allow cannot. Status honors `network.egress.status` and `egress` policy. MCP hosts pass lifecycle authority and action input to the policy-key helper, then use `resolveSandboxToolPolicy` so any applicable Ask wins for Configure/Disable. Status retains ordinary first-match read-policy precedence for `network.egress.status` and `egress`. This release exposes status on write/lifecycle bindings. Foreground actor changes suspend as the currently acting user, including a trusted userless host, and block until an explicit route choice; background bindings do not revoke another actor's chosen route. Userless execution cannot inherit someone else's Device access.
87
+
88
+ Read bindings skip recovery and deferred setup. Pass binding access to `withWorkspaceEgress`; never initialize a manager for a read binding. They may inspect files but cannot repair a blocked route or run setup. A write binding must configure or explicitly disable first. Networking changes do not run deferred repository/setup work; that runs before the next guarded execution, write, or foreground MCP endpoint/channel acquisition.
89
+
90
+ The manager owns renewal; failure observers cannot interrupt networking or claim cleanup. Editor access by another actor is refused before changing the owner’s route. A successful direct-policy retry clears a previous provider-update failure, including for userless workspaces. `close()` detaches a handle; runtime shutdown must not disrupt another binding sharing the workspace. `close(true)` attempts the namespace fence and suspends every lease for an explicit workspace stop/deletion, even if its cached lease has been replaced. Failure cleanup revokes only a lease that this handle created. Provider teardown independently attempts the fence, resource cleanup and lease revocation, and reports failures after cleanup. Removing boot admission must still permit teardown; hosts distinguish it with the predicate's `teardown` flag. Desired settings survive stop and lease expiry.
91
+
92
+ Teardown preserves the current provider policy before applying a fence. If eligibility or initial-policy persistence is unavailable for an unknown workspace, teardown still stops the resource, attempts suspension, and reports the error; it does not overwrite an unrelated policy. Host-managed boot waits for eligibility and saved policy recovery. Managed Computer creation and write/resume disable automatic wake. Passive read attachments do not change provider settings; migrate existing resources through a write attachment before relying on managed wake protection. Daytona’s preferred-fence fallback retains the documented startup window.
93
+
94
+ Handles opened before opt-in check admission before new commands, stdin, policy changes, and MCP messages; they must reconnect when the shared workspace becomes managed. Existing processes and previously issued editor/preview credentials still share that namespace and are not revoked by opting in or changing the exit. Hosts must close existing interactive sessions before allowing a different actor to use a personal Device on a shared workspace. Editor grants report a refresh deadline at least ninety seconds before lease expiry; refresh rechecks and renews networking, while closing an editor detaches without revoking an agent’s shared lease. Both providers persist management learned after start when parking; the durable host registration remains authoritative across restarts. Unsupported provider fences intentionally leave namespace-only denial; that retains the documented boot window, and does not authorize physical egress after guard readiness.
95
+
96
+ A failed route mutation from another actor must pass a fresh Device API authorization check before changing the namespace. The current actor's still-active owned lease permits local cleanup during a control-plane outage. MCP batches received before a transient guard failure are retained until a later authorized receive; business requests are never replayed.
97
+
98
+ ## Controller, recovery and status
99
+
100
+ The controller verifies helper hashes/architecture, maintains physical-egress and IPv6 denial, and repairs managed DNS after network-manager rewrites. Disable restores resolver/firewall ownership, including after loss of volatile state, and clears persisted identity. Only discovered namespace-local nested bridges may forward through the exit; unrelated physical hosts cannot use the workspace as a router. Cooperative root/NET_ADMIN workloads can override their own namespace, so this is not a security boundary against hostile root.
101
+
102
+ The enrollment API returns `provisioning`; activation alone publishes grants and returns `active`. Configure and disable serialize through a bounded root-owned claim, refreshed every minute and expiring within five minutes after its last refresh. A superseded operation cannot activate an enrollment or undo a later disable. Each claim-acquisition or controller-stability wait is capped at three minutes; recovery can perform more than one wait. Lease deadlines use server wall time; retry throttles use monotonic time. Clocks must stay within one minute of the server.
103
+
104
+ A live, nonexpired controller survives exit/control outages without repeated helper transfers. Renewal attempts are spaced at least 30 seconds apart; failed automatic re-enrollment has a 60-second cooldown. Explicit configure can retry sooner. Failed control-state recovery has a separate five-second backoff across guarded calls. Foreground operations and delivered MCP traffic with an active route, including stdin writes and MCP messages, recheck namespace ownership through a privileged provider exec with a 100 ms cache. Recovery/configuration serializes them. Direct unconfigured handles make no background namespace probes. Handle lifetime bounds host renewal.
105
+
106
+ Workspace helpers enable shields-up to reject unsolicited tunnel connections independently of control-server grants. Recovery after a crashed namespace controller can take about thirty seconds and retains denial throughout. The default privileged launcher sends source and input on stdin, keeping shell arguments small. Custom argv-based executors must stay below Linux’s per-argument size limit; cached execution uses a short digest-pinned loader.
107
+
108
+ Status reports error when no denial tier can be installed; blocked is reserved for confirmed namespace denial. Configuration validation after guard installation leaves blocked status, rather than stale direct/device status. Cleanup errors are retained with the original failure and reported through onFailure.
109
+
110
+ ## MCP and editor transports
111
+
112
+ Managed screen grants are view-only; interactive input uses the guarded computer-use MCP channel. URL MCP endpoints are converted to owned message channels so actor and policy checks also apply to cached endpoints.
113
+
114
+ Managed MCP uses guarded message channels. Hosted tools named `egress`, `network.egress`, or its configure/disable/status keys receive their server-name prefix in projections, retaining the ordinary hosted-tool approval policy. Handle RPC clients must negotiate `mcpChannels` for local MCP/computer-use, or use the MCP projection; legacy URL-only clients do not receive an unguarded URL. Known unsupported provider firewalls cannot restore a restrictive direct-access policy: Disable leaves namespace traffic blocked and reports the missing provider capability rather than claiming restoration succeeded.
115
+
116
+ Generic MCP channel transports poll while idle (with backoff capped at one second) to deliver server notifications and requests. Hosted discovery clients consume messages only while requests are pending; explicit catalog reads refresh the server listing. URL-backed HTTP transports may independently reconnect their notification stream, which rechecks admission and can refresh a Device lease. Cache hosts should close idle channels if they require strictly passive discovery. These private catalog clients do not service idle server requests or guarantee idle notifications; servers may end an idle session. An already-ended owned HTTP channel reconnects before sending a business request. Once a request may have been sent, failure is reported without replay; the following call reconnects. Buffer limits deliberately close a flooding channel. New HTTP streams and received messages also recheck admission. A notification-stream error alone does not cancel independent tool requests. A terminal receive failure closes the channel and evicts its cached client; the next call reconnects without replaying a tool invocation. Close cached clients when their handle closes. URL-backed MCP channels retain the SDK request timeout and per-request SSE recovery behavior; a failed POST does not fail unrelated requests. Each parsed receive queue rejects batches exceeding 4,096 messages or 16 MiB. This bounds retained queues per guard layer, not the HTTP/SSE parser’s allocation before delivery; hosts must bound transport response bodies independently. Closing attempts bounded server-session termination, then closes the local transport.
117
+
118
+ Background MCP queue polling retains the last authorized owner and does not run workspace setup for empty queues. Delivered messages and stream reconnects pass through admission; active routes also recheck namespace ownership. Direct and choice-blocked handles skip background namespace probes. Business requests still require the current foreground actor; catalog/protocol replies retain the last authorized owner. Opening a hosted endpoint initially requires foreground admission; hosts may authorize those discovery operations explicitly. Automatic retries of incomplete catalogs have a one-second cooldown; explicit catalog refreshes retry immediately. Failed deferred setup is reported and retried on the next guarded use. Setup steps must tolerate partial execution, as during host recovery. Generic workspace IDs may be opaque strings; Device egress requires a stable ID of at most 256 characters with no control characters.
119
+
120
+ ## Provider prerequisites and limits
121
+
122
+ Custom images require root, the netfilter owner match, Python 3.8 or newer, TUN, IPv4 policy routing, `iproute2`, and IPv4/IPv6 netfilter before opting in. Direct status/disable remain usable on images without iptables; blocking and restoration of an installed guard still require netfilter tools. Blocking/disable do not require TUN. A missing executor or prerequisite cannot silently remove a guard; repair the image or use the provider's administrative API. Disabling automatic wake on a managed Computer is deliberate: boot must pass through its fence. Providers claiming boot fencing must verify its behavior on creation, stop/archive and resume; an accepted API request alone is insufficient. Preferred fencing may instead log the documented startup-window fallback. Paid-provider tests are manual and opt-in. Modal's tested VM kernel lacks policy routing and is rejected before enrollment.
123
+
124
+ Install the guard before exceeding 128 existing external control connections. Connection overflow installs the closed full guard before returning an error, falling back to minimal denial if needed. Emergency denial preserves loopback except DNS stubs and ACK replies on captured inbound control sockets. If no control socket could be captured, use the provider administrative API to repair the image. Device-mode listener discovery takes a few seconds. New preview listeners opened while blocked need another configure/block refresh. Helper router-mapping cleanup is best effort on graceful shutdown; abrupt process death or router failure can leave mappings for the router to remove.
125
+
126
+ Existing-connection overflow applies during capture as well as configuration and installs denial before reporting failure. Missing firewall tools require repair through the provider administrative API; absent legacy NAT/mangle modules can be restored from filter-only denial.
127
+
128
+ Existing untracked provider-control reply exceptions require root-owned established sockets; non-root listeners cannot preserve a physical outbound connection. Normally tracked inbound services still use conntrack reply direction, including services running as ordinary users. A failed temporary-artifact removal is treated as failed configuration and rolls back the route; fix the provider file API and retry.
129
+
130
+ The controller preserves at most 256 inbound TCP listener ports, preferring already retained ports and privileged provider listeners. Additional listeners do not crash the controller; their physical-network replies may be blocked. A root-owned `listener-limit.json` diagnostic records saturation. This limit does not affect outbound connections through the Device.
131
+
132
+ The workspace helper internal DNS address (100.100.100.100) is blocked; configure a Device-reachable DNS server instead. MagicDNS and host split-DNS integration remain outside this capability.
133
+
134
+ The local kernel acceptance image uses Python 3.13; Python 3.8 is the controller syntax/stdlib floor, rather than a separately exercised provider image. Run manual prerequisite acceptance for each custom image before rollout.
135
+
136
+ ## Compatibility and repair
137
+
138
+ Configuring workspace admission requires MCP channel negotiation (or the MCP projection) even for currently unmanaged write handles. Admission checks intentionally run before new commands and messages so handles opened before opt-in cannot bypass a later change; an unavailable admission authority refuses execution. Legacy URL-only handle-RPC clients must upgrade before enabling this host integration. Read handles remain passive. `onSandboxStop` and `onSandboxDelete` represent authorized physical lifecycle actions; viewer disconnect and runtime detach use `onRuntimeShutdown`.
139
+
140
+ Controller error status prevents guarded execution and triggers authorized recovery. If the local enrollment deadline has already elapsed, renewal requests fresh enrollment rather than restarting without credentials. A lost original DNS backup requires provider-administrative resolver repair; the controller never treats managed resolver content as the original. Resolver restoration replaces an unexpected dangling symlink without following its target.
141
+
142
+ Raw guarded MCP channels retain received batches until a later authorized receive. The hosted MCP client treats a receive or authorization failure as terminal and reports uncertainty to outstanding callers; reconnect on the next operation, without replaying a business call. Transport-only notification-stream errors leave independent POSTs alone; failed admission closes the whole owned channel deliberately.
143
+
144
+ The namespace must remain under the adapter's networking ownership. External privileged firewall flushes/reloads and direct provider or in-guest reboots bypass this lifecycle; stop execution and initialize the adapter again after such operations. Blocked status records successful denial installation by the owned controller, not continuous attestation against a privileged process rewriting the firewall. This mechanism is not a boundary against namespace root/NET_ADMIN. Supported nested-container filter reordering remains covered by the earlier mangle guard.
145
+
146
+ Workspace identity must be stable before the first managed start. Changing from a derived identity to another ID while reusing the same physical resource is an unsupported identity migration; disable and retire the old managed workspace first. The SDK does not infer authorization or a saved policy from provider-private state. Passive recovery is a read-only viewer operation for all hosts, independent of egress opt-in, matching the provider interface contract.
147
+
148
+ A claim's five-minute expiry is a renewable lease, not an absolute total operation deadline. Hosts must provide bounded custom execution/upload/restoration hooks; a live owner awaiting an unbounded custom promise can keep refreshing its claim. Built-in public API calls have request deadlines. Failure cleanup attempts provider deny-all when namespace denial cannot be confirmed. With confirmed namespace denial, an unrestricted saved provider policy may be restored while the namespace stays blocked; Disable separately restores direct namespace access and the fresh saved policy.
149
+
150
+ Computer service bookkeeping uses the raw setup session's public file operation, whose directory creation waits through the public process API before the file write. It runs before deferred repository or service setup, including on fenced fresh read attachments. Helper publication atomically installs executable permissions so a concurrent restart cannot observe a temporary non-executable binary.
151
+
152
+ Disabling and reconfiguring is required after changing namespace bridge layouts,
153
+ so stale owned routing rules are removed. Reinitialize after enabling IPv6 kernel
154
+ support that was unavailable during the controller prerequisite probe. Symlinked
155
+ `/etc/hosts` images require a regular hosts file before Device configuration.
156
+
157
+ Supply `fetch` to `withWorkspaceEgress`/`withWorkspaceAdmission` for a custom MCP
158
+ transport; Computer integrations supply `networking.fetch` through `createWorkspaceDeviceNetworking({ mcpFetch })`. Artifact download
159
+ fetch and MCP fetch are independent dependencies. A first failed attempt without
160
+ NET_ADMIN can be explicitly disabled when the controller proves no networking
161
+ changes were possible; prior managed namespaces still require privileged repair.
162
+
163
+ Namespace-local bridge subnets take precedence over exit routes. Use bridge ranges
164
+ that do not overlap private destinations you need to reach through the Device.
165
+
166
+ Daytona cannot verify pre-boot enforcement from control-plane policy readback for a stopped sandbox. Its default resume path logs the startup window, starts the sandbox, then installs provider and namespace denial before agent tools. Explicitly requiring a verified boot fence refuses that stopped resume.
167
+
168
+ The default Computer and Daytona executors use passwordless sudo in the workload session. This is cooperative routing, not a security boundary against that workload: it can change its own firewall. For containment, supply an out-of-band privilegedRun through the composable controller and remove workload root/sudo and CAP_NET_RAW/CAP_NET_ADMIN. AF_PACKET frames can bypass IP netfilter; the raw-IP packet probes do not establish containment for link-layer injection. Killing processes and closing existing access remain available while execution admission is denied.
169
+
170
+ The retained direct policy is the actual provider policy, including existing platform-required destinations. Device policy persistence supports at most 1,024 entries of 253 characters; managed policy admission rejects unsupported snapshots before changing provider rules. Hosts own complete RPC authorization through authorizeOperation, including any consent required for Egress. An audit-only hook is not an authorization policy.
171
+
172
+ Custom privileged executors must launch the trusted system interpreter with a sanitized environment, including loader variables, before running supplied Python source. The controller additionally pins system command lookup and strips custom TLS trust overrides from its owned helpers.
@@ -7,4 +7,5 @@ export declare const computerEditor: (args: {
7
7
  computerId: string;
8
8
  request: SandboxEditorRequestIface;
9
9
  randomUuid: () => string;
10
+ expiresAtMs?: number;
10
11
  }) => Promise<SandboxEditorResultIface>;
@@ -139,8 +139,18 @@ const computerEditor = async (args) => {
139
139
  try {
140
140
  const access = await client.openBrowserAccess(computerId, {
141
141
  port: COMPUTER_EDITOR_PORT,
142
- returnTo: `/?folder=${encodeURIComponent("/workspaces")}`
142
+ returnTo: `/?folder=${encodeURIComponent("/workspaces")}`,
143
+ ...args.expiresAtMs == null ? {} : { expiresAt: new Date(args.expiresAtMs).toISOString() }
143
144
  });
145
+ if (args.expiresAtMs != null && (!Number.isFinite(Date.parse(access.expiresAt)) || Date.parse(access.expiresAt) > args.expiresAtMs)) {
146
+ const failure = new Error("Computers API did not enforce the editor authorization deadline");
147
+ try {
148
+ await client.closeBrowserAccess(computerId, access.tokenId);
149
+ } catch (cleanup) {
150
+ throw new AggregateError([failure, cleanup], failure.message);
151
+ }
152
+ throw failure;
153
+ }
144
154
  return {
145
155
  id: result.id,
146
156
  authorizationId: access.tokenId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/sandboxes/computer-editor.ts"],
4
- "sourcesContent": ["import {\n SandboxEditorOperation,\n type SandboxEditorRequestIface,\n type SandboxEditorResultIface,\n} from '@finalai/agent-provider';\nimport { ComputerState } from '@finalai/computers';\nimport * as z from 'zod';\nimport { isComputersApiStatus, type ComputersApiClientIface } from \"./computers-api.mjs\";\nimport { shellQuote } from \"./paths.mjs\";\n\nexport const COMPUTER_EDITOR_PORT = 9910;\nconst resultSchema = z.object({ id: z.string().uuid(), closed: z.boolean() });\n\n// Backend-owned editor/process leases survive a provider reconnect. Nothing is written\n// into the workspace, and bwrap grants the same workspace/home mounts as the old editor.\nexport const computerEditorScript = String.raw`\nimport fcntl, json, os, pathlib, shutil, signal, socket, subprocess, sys, time, uuid\nrequest = json.loads(sys.argv[1])\neditor_id = str(uuid.UUID(request['id']))\nroot = pathlib.Path('/tmp/final-computer-editor')\nroot.mkdir(mode=0o700, exist_ok=True)\nif root.is_symlink() or root.stat().st_uid != os.getuid():\n raise RuntimeError('unsafe editor state directory')\nlock_fd = os.open(str(root / 'lock'), os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)\nwith os.fdopen(lock_fd, 'r+') as lock:\n fcntl.flock(lock, fcntl.LOCK_EX)\n record_path = root / 'session.json'\n state = {'pid': None, 'startTicks': None, 'sessions': {}}\n if record_path.exists():\n with os.fdopen(os.open(str(record_path), os.O_RDONLY | os.O_NOFOLLOW)) as source:\n state = json.load(source)\n def live():\n pid = state.get('pid')\n if not isinstance(pid, int) or pid <= 1: return False\n try:\n stat = pathlib.Path('/proc/' + str(pid) + '/stat').read_text()\n ticks = stat.rsplit(')', 1)[1].split()[19]\n command = pathlib.Path('/proc/' + str(pid) + '/cmdline').read_bytes()\n return ticks == state.get('startTicks') and str(root / 'data').encode() in command\n except (FileNotFoundError, ProcessLookupError): return False\n def stop():\n if live():\n os.killpg(state['pid'], signal.SIGTERM)\n state['pid'] = None\n state['startTicks'] = None\n now = time.time()\n sessions = {key: deadline for key, deadline in state.get('sessions', {}).items() if deadline > now}\n operation = request['operation']\n if operation != 'open' and editor_id not in sessions:\n if operation == 'close':\n print(json.dumps({'id': editor_id, 'closed': True}))\n sys.exit(0)\n raise RuntimeError('editor session expired')\n if operation == 'close':\n sessions.pop(editor_id, None)\n if not sessions: stop()\n else:\n if not live():\n if operation == 'refresh': raise RuntimeError('editor process ended; reopen the editor')\n sessions = {}\n code_server = shutil.which('code-server')\n if code_server is None:\n sandbox = shutil.which('final-node-sandbox')\n candidate = None if sandbox is None else pathlib.Path(os.path.realpath(sandbox)).parent.parent / 'code-server/bin/code-server'\n if candidate is not None and candidate.is_file(): code_server = str(candidate)\n bwrap = shutil.which('bwrap')\n if code_server is None or bwrap is None: raise RuntimeError('code-server and bwrap must be installed')\n workspace, home = '/workspaces', '/home/user'\n for path in (workspace, home):\n if os.path.realpath(path) != path or not os.path.isdir(path): raise RuntimeError('unsafe editor workspace')\n data, extensions = str(root / 'data'), str(root / 'extensions')\n os.makedirs(data, mode=0o700, exist_ok=True)\n os.makedirs(extensions, mode=0o700, exist_ok=True)\n command = [bwrap, '--new-session', '--unshare-pid', '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp']\n for path in ('/bin', '/etc', '/lib', '/lib64', '/nix', '/opt', '/sbin', '/usr'):\n if os.path.exists(path): command += ['--ro-bind', path, path]\n for path in ('/home', home, str(root), data, extensions): command += ['--dir', path]\n command += ['--bind', workspace, workspace, '--bind', home, home, '--bind', data, data, '--ro-bind', extensions, extensions]\n if os.path.exists('/etc/final-vm'): command += ['--tmpfs', '/etc/final-vm']\n command += ['--setenv', 'HOME', home, '--setenv', 'CODE_SERVER_APP_NAME', 'Final Code', '--setenv', 'EXTENSIONS_GALLERY', '{}', '--chdir', workspace,\n code_server, '--bind-addr', '0.0.0.0:9910', '--auth', 'none', '--disable-telemetry', '--disable-update-check', '--disable-proxy', '--ignore-last-opened',\n '--idle-timeout-seconds', '1200', '--user-data-dir', data, '--extensions-dir', extensions, workspace]\n try:\n with socket.create_connection(('127.0.0.1', 9910), timeout=0.25):\n raise RuntimeError('editor port is already in use')\n except OSError: pass\n environment = {key: os.environ[key] for key in ('PATH', 'LANG', 'LC_ALL', 'SHELL', 'TERM', 'COLORTERM') if key in os.environ}\n process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=environment, start_new_session=True)\n state['pid'] = process.pid\n state['startTicks'] = pathlib.Path('/proc/' + str(process.pid) + '/stat').read_text().rsplit(')', 1)[1].split()[19]\n deadline = time.monotonic() + 15\n ready = False\n while time.monotonic() < deadline:\n if process.poll() is not None: break\n try:\n with socket.create_connection(('127.0.0.1', 9910), timeout=0.25): ready = True; break\n except OSError: time.sleep(0.1)\n if not ready:\n stop()\n raise RuntimeError('editor did not start; port 9910 must be available')\n sessions[editor_id] = now + 900\n state['sessions'] = sessions\n temporary = root / ('session.' + str(uuid.uuid4()) + '.json')\n with os.fdopen(os.open(str(temporary), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600), 'w') as target:\n json.dump(state, target)\n os.replace(temporary, record_path)\n print(json.dumps({'id': editor_id, 'closed': operation == 'close'}))\n`;\n\nexport const computerEditor = async (args: {\n client: ComputersApiClientIface;\n computerId: string;\n request: SandboxEditorRequestIface;\n randomUuid: () => string;\n}): Promise<SandboxEditorResultIface> => {\n const { client, computerId, request } = args;\n const info = await client.getComputer(computerId);\n if (request.operation === SandboxEditorOperation.Available)\n return { available: info?.state === ComputerState.Running };\n if (\n request.operation === SandboxEditorOperation.Close &&\n (info == null || info.state !== ComputerState.Running)\n ) {\n try {\n await client.closeBrowserAccess(computerId, request.authorizationId);\n } catch (cause) {\n if (!isComputersApiStatus(cause, 404)) throw cause;\n }\n return { closed: true };\n }\n if (info == null || info.state !== ComputerState.Running)\n throw new Error('computer is not running for editor access');\n const id = request.operation === SandboxEditorOperation.Open ? args.randomUuid() : request.id;\n const run = async (operation: SandboxEditorOperation) => {\n const result = await client.run(computerId, {\n command: `python3 -c ${shellQuote(computerEditorScript)} ${shellQuote(JSON.stringify({ operation, id }))}`,\n cwd: '/workspaces',\n user: 'user',\n autoResume: false,\n timeoutMs: 25_000,\n waitMs: 30_000,\n });\n if (result.timedOut || result.state !== 'exited' || result.result?.exitCode !== 0)\n throw new Error('computer editor operation failed');\n return resultSchema.parse(\n JSON.parse(Buffer.from(result.stdout.dataB64, 'base64').toString('utf8')),\n );\n };\n const result = await run(request.operation);\n if (request.operation === SandboxEditorOperation.Close) {\n await client.closeBrowserAccess(computerId, request.authorizationId);\n return { closed: true };\n }\n try {\n const access = await client.openBrowserAccess(computerId, {\n port: COMPUTER_EDITOR_PORT,\n returnTo: `/?folder=${encodeURIComponent('/workspaces')}`,\n });\n return {\n id: result.id,\n authorizationId: access.tokenId,\n workingDirectory: '/workspaces',\n url: new URL(access.returnTo, access.url).toString(),\n expiresAt: access.expiresAt,\n authentication: {\n url: access.url,\n fields: { token: access.token, returnTo: access.returnTo },\n },\n };\n } catch (cause) {\n if (request.operation === SandboxEditorOperation.Open)\n await run(SandboxEditorOperation.Close).catch(() => undefined);\n throw cause;\n }\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n SandboxEditorOperation,\n type SandboxEditorRequestIface,\n type SandboxEditorResultIface,\n} from '@finalai/agent-provider';\nimport { ComputerState } from '@finalai/computers';\nimport * as z from 'zod';\nimport { isComputersApiStatus, type ComputersApiClientIface } from \"./computers-api.mjs\";\nimport { shellQuote } from \"./paths.mjs\";\n\nexport const COMPUTER_EDITOR_PORT = 9910;\nconst resultSchema = z.object({ id: z.string().uuid(), closed: z.boolean() });\n\n// Backend-owned editor/process leases survive a provider reconnect. Nothing is written\n// into the workspace, and bwrap grants the same workspace/home mounts as the old editor.\nexport const computerEditorScript = String.raw`\nimport fcntl, json, os, pathlib, shutil, signal, socket, subprocess, sys, time, uuid\nrequest = json.loads(sys.argv[1])\neditor_id = str(uuid.UUID(request['id']))\nroot = pathlib.Path('/tmp/final-computer-editor')\nroot.mkdir(mode=0o700, exist_ok=True)\nif root.is_symlink() or root.stat().st_uid != os.getuid():\n raise RuntimeError('unsafe editor state directory')\nlock_fd = os.open(str(root / 'lock'), os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)\nwith os.fdopen(lock_fd, 'r+') as lock:\n fcntl.flock(lock, fcntl.LOCK_EX)\n record_path = root / 'session.json'\n state = {'pid': None, 'startTicks': None, 'sessions': {}}\n if record_path.exists():\n with os.fdopen(os.open(str(record_path), os.O_RDONLY | os.O_NOFOLLOW)) as source:\n state = json.load(source)\n def live():\n pid = state.get('pid')\n if not isinstance(pid, int) or pid <= 1: return False\n try:\n stat = pathlib.Path('/proc/' + str(pid) + '/stat').read_text()\n ticks = stat.rsplit(')', 1)[1].split()[19]\n command = pathlib.Path('/proc/' + str(pid) + '/cmdline').read_bytes()\n return ticks == state.get('startTicks') and str(root / 'data').encode() in command\n except (FileNotFoundError, ProcessLookupError): return False\n def stop():\n if live():\n os.killpg(state['pid'], signal.SIGTERM)\n state['pid'] = None\n state['startTicks'] = None\n now = time.time()\n sessions = {key: deadline for key, deadline in state.get('sessions', {}).items() if deadline > now}\n operation = request['operation']\n if operation != 'open' and editor_id not in sessions:\n if operation == 'close':\n print(json.dumps({'id': editor_id, 'closed': True}))\n sys.exit(0)\n raise RuntimeError('editor session expired')\n if operation == 'close':\n sessions.pop(editor_id, None)\n if not sessions: stop()\n else:\n if not live():\n if operation == 'refresh': raise RuntimeError('editor process ended; reopen the editor')\n sessions = {}\n code_server = shutil.which('code-server')\n if code_server is None:\n sandbox = shutil.which('final-node-sandbox')\n candidate = None if sandbox is None else pathlib.Path(os.path.realpath(sandbox)).parent.parent / 'code-server/bin/code-server'\n if candidate is not None and candidate.is_file(): code_server = str(candidate)\n bwrap = shutil.which('bwrap')\n if code_server is None or bwrap is None: raise RuntimeError('code-server and bwrap must be installed')\n workspace, home = '/workspaces', '/home/user'\n for path in (workspace, home):\n if os.path.realpath(path) != path or not os.path.isdir(path): raise RuntimeError('unsafe editor workspace')\n data, extensions = str(root / 'data'), str(root / 'extensions')\n os.makedirs(data, mode=0o700, exist_ok=True)\n os.makedirs(extensions, mode=0o700, exist_ok=True)\n command = [bwrap, '--new-session', '--unshare-pid', '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp']\n for path in ('/bin', '/etc', '/lib', '/lib64', '/nix', '/opt', '/sbin', '/usr'):\n if os.path.exists(path): command += ['--ro-bind', path, path]\n for path in ('/home', home, str(root), data, extensions): command += ['--dir', path]\n command += ['--bind', workspace, workspace, '--bind', home, home, '--bind', data, data, '--ro-bind', extensions, extensions]\n if os.path.exists('/etc/final-vm'): command += ['--tmpfs', '/etc/final-vm']\n command += ['--setenv', 'HOME', home, '--setenv', 'CODE_SERVER_APP_NAME', 'Final Code', '--setenv', 'EXTENSIONS_GALLERY', '{}', '--chdir', workspace,\n code_server, '--bind-addr', '0.0.0.0:9910', '--auth', 'none', '--disable-telemetry', '--disable-update-check', '--disable-proxy', '--ignore-last-opened',\n '--idle-timeout-seconds', '1200', '--user-data-dir', data, '--extensions-dir', extensions, workspace]\n try:\n with socket.create_connection(('127.0.0.1', 9910), timeout=0.25):\n raise RuntimeError('editor port is already in use')\n except OSError: pass\n environment = {key: os.environ[key] for key in ('PATH', 'LANG', 'LC_ALL', 'SHELL', 'TERM', 'COLORTERM') if key in os.environ}\n process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=environment, start_new_session=True)\n state['pid'] = process.pid\n state['startTicks'] = pathlib.Path('/proc/' + str(process.pid) + '/stat').read_text().rsplit(')', 1)[1].split()[19]\n deadline = time.monotonic() + 15\n ready = False\n while time.monotonic() < deadline:\n if process.poll() is not None: break\n try:\n with socket.create_connection(('127.0.0.1', 9910), timeout=0.25): ready = True; break\n except OSError: time.sleep(0.1)\n if not ready:\n stop()\n raise RuntimeError('editor did not start; port 9910 must be available')\n sessions[editor_id] = now + 900\n state['sessions'] = sessions\n temporary = root / ('session.' + str(uuid.uuid4()) + '.json')\n with os.fdopen(os.open(str(temporary), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600), 'w') as target:\n json.dump(state, target)\n os.replace(temporary, record_path)\n print(json.dumps({'id': editor_id, 'closed': operation == 'close'}))\n`;\n\nexport const computerEditor = async (args: {\n client: ComputersApiClientIface;\n computerId: string;\n request: SandboxEditorRequestIface;\n randomUuid: () => string;\n expiresAtMs?: number;\n}): Promise<SandboxEditorResultIface> => {\n const { client, computerId, request } = args;\n const info = await client.getComputer(computerId);\n if (request.operation === SandboxEditorOperation.Available)\n return { available: info?.state === ComputerState.Running };\n if (\n request.operation === SandboxEditorOperation.Close &&\n (info == null || info.state !== ComputerState.Running)\n ) {\n try {\n await client.closeBrowserAccess(computerId, request.authorizationId);\n } catch (cause) {\n if (!isComputersApiStatus(cause, 404)) throw cause;\n }\n return { closed: true };\n }\n if (info == null || info.state !== ComputerState.Running)\n throw new Error('computer is not running for editor access');\n const id = request.operation === SandboxEditorOperation.Open ? args.randomUuid() : request.id;\n const run = async (operation: SandboxEditorOperation) => {\n const result = await client.run(computerId, {\n command: `python3 -c ${shellQuote(computerEditorScript)} ${shellQuote(JSON.stringify({ operation, id }))}`,\n cwd: '/workspaces',\n user: 'user',\n autoResume: false,\n timeoutMs: 25_000,\n waitMs: 30_000,\n });\n if (result.timedOut || result.state !== 'exited' || result.result?.exitCode !== 0)\n throw new Error('computer editor operation failed');\n return resultSchema.parse(\n JSON.parse(Buffer.from(result.stdout.dataB64, 'base64').toString('utf8')),\n );\n };\n const result = await run(request.operation);\n if (request.operation === SandboxEditorOperation.Close) {\n await client.closeBrowserAccess(computerId, request.authorizationId);\n return { closed: true };\n }\n try {\n const access = await client.openBrowserAccess(computerId, {\n port: COMPUTER_EDITOR_PORT,\n returnTo: `/?folder=${encodeURIComponent('/workspaces')}`,\n ...(args.expiresAtMs == null ? {} : { expiresAt: new Date(args.expiresAtMs).toISOString() }),\n });\n if (\n args.expiresAtMs != null &&\n (!Number.isFinite(Date.parse(access.expiresAt)) ||\n Date.parse(access.expiresAt) > args.expiresAtMs)\n ) {\n const failure = new Error('Computers API did not enforce the editor authorization deadline');\n try {\n await client.closeBrowserAccess(computerId, access.tokenId);\n } catch (cleanup) {\n throw new AggregateError([failure, cleanup], failure.message);\n }\n throw failure;\n }\n return {\n id: result.id,\n authorizationId: access.tokenId,\n workingDirectory: '/workspaces',\n url: new URL(access.returnTo, access.url).toString(),\n expiresAt: access.expiresAt,\n authentication: {\n url: access.url,\n fields: { token: access.token, returnTo: access.returnTo },\n },\n };\n } catch (cause) {\n if (request.operation === SandboxEditorOperation.Open)\n await run(SandboxEditorOperation.Close).catch(() => undefined);\n throw cause;\n }\n};\n"],
5
+ "mappings": "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",
6
6
  "names": ["result"]
7
7
  }
@@ -1,9 +1,15 @@
1
1
  import { SandboxAccess, type JsonValue } from '@finalai/agent-provider';
2
- import { SandboxCapability, type NetworkSandboxSessionIface, type SandboxProviderIface } from '@finalai/agent-provider/sandbox';
2
+ import { type SandboxNetworkingExtensionIface, SandboxCapability, type NetworkSandboxSessionIface, type SandboxProviderIface } from '@finalai/agent-provider/sandbox';
3
3
  import * as z from 'zod';
4
4
  import { type ComputersApiConfigIface } from "./computers-api.mjs";
5
5
  import { ComputerWorkspacePolicy } from "./environment.mjs";
6
6
  import { type GuideCommandIface } from "./workspace-setup.mjs";
7
+ export declare const computerWorkspaceName: (options: {
8
+ bindingId: string;
9
+ conversationId: string | null;
10
+ target: string;
11
+ conversationWorkspace: boolean;
12
+ }) => Promise<string>;
7
13
  export type { ComputersApiConfigIface, ComputersApiCredentialIface } from "./computers-api.mjs";
8
14
  export { COMPUTER_SANDBOX_SPECIFIER, COMPUTERS_API_KEY_SECRET, COMPUTERS_API_TEAM_SECRET, } from "./environment.mjs";
9
15
  export { computeComputerArtifactKey } from "./guide.mjs";
@@ -14,6 +20,7 @@ export interface ComputerSandboxLogIface {
14
20
  }
15
21
  export interface ComputerSandboxProviderConfigIface {
16
22
  computersApi: ComputersApiConfigIface;
23
+ networking?: SandboxNetworkingExtensionIface;
17
24
  registryCredentialId?: string;
18
25
  defaultImage?: string;
19
26
  regionSlug?: string;
@@ -63,6 +70,9 @@ declare const computerStateSchema: z.ZodObject<{
63
70
  access: z.ZodEnum<typeof SandboxAccess>;
64
71
  guideVersionId: z.ZodNullable<z.ZodString>;
65
72
  setupHash: z.ZodNullable<z.ZodString>;
73
+ setupPending: z.ZodDefault<z.ZodBoolean>;
74
+ workspaceId: z.ZodOptional<z.ZodString>;
75
+ networkPolicySaved: z.ZodOptional<z.ZodBoolean>;
66
76
  }, z.core.$strip>;
67
77
  export type ComputerSandboxArtifact = z.infer<typeof computerArtifactSchema>;
68
78
  export type ComputerSandboxState = z.infer<typeof computerStateSchema>;