/** * Remote execution seam (design/48 §5) — the type-only contract a **remote/containerised** `ExecutionEnv` * implementation (E2B/Firecracker, owned by the service control plane) must satisfy so the agent's "hand" * (design/44) can act inside an isolated, *stateful*, *suspendable* workspace instead of the in-process * `NodeExecutionEnv`. * * **This file is a SEAM, not an implementation.** The real `RemoteContainerExecutionEnv` (E2B SDK, warm * pools, snapshot lifecycle, tier routing) lives in the service and is driven by real integration needs — * exactly like `CheckpointStore` (design/45). Core owns the seam shape; service owns the backend. * * Design rulings folded in (design/48 §10/§11 DeepSeek council): * - **#1 [blocker] interface extension, not capability probing**: `RemoteExecutionEnv extends ExecutionEnv`. * The base {@link ExecutionEnv} is UNCHANGED — `NodeExecutionEnv`/`StubExecutionEnv` need zero edits, and a * consumer that only needs buffered fs/shell keeps accepting the base type. * - **#3 [major] do not overload `exec`**: `exec()` stays buffered; streaming is a NEW {@link execStream}. * - **#4 [major] suspend × in-flight exec must not hang**: see {@link suspendVM}. * - **#8 [major] post-resume consistency**: see {@link postResumeInit}. * - **#10 naming**: the isolation level is `sandboxTier` (not `tier`) to avoid clashing with the existing * timeout-/degradation-"tier" in the codebase. * - **#16 suspend/resume disambiguation**: the VM-lifecycle methods are `suspendVM`/`resumeVM` to keep them * distinct from the Runner gate-resume and design/45 wake/resume semantics. * * **Deferred (NOT modelled here — they are runtime, not seam):** actually suspending a running task and * persisting the {@link WorkspaceHandle} into a design/45 `Checkpoint`; the `status:"suspended"` orchestrator * mapping; tier *classification* (the static table + Tier1-safe allow-list + fail-closed routing) — all are * service/design-45 implementation-time concerns. Core only declares the seam those will plug into. */ import type { ExecutionEnv } from "../internal/harness.js"; /** Snapshot identifier returned by {@link RemoteExecutionEnv.suspendVM}; opaque to core, resolved by the provider. */ export type SnapshotId = string; /** * Token identifying a still-running remote session, used by {@link RemoteExecutionEnv.reconnect} to re-attach * the control plane's transport after a control-plane replica crash/failover. The underlying VM state lives * independently of any single control-plane TCP connection (a Firecracker process survives a dropped client), * so reconnect re-binds rather than re-creates. */ export type SessionToken = string; /** Isolation routing level (design/48 §2). v1 runs everything as Tier 2; classification is DEFERRED to service. */ export type SandboxTier = 1 | 2; /** * Serializable identity of a remote workspace (design/48 §5 gap 1). The control plane persists this into its * durable state (and design/45 `Checkpoint`) so a *different* replica can reconnect/resume the same workspace. */ export interface WorkspaceHandle { /** Provider/VM/sandbox id (e.g. an E2B sandbox id). */ sandboxId: string; /** Provider identifier (e.g. `"e2b"`), so the control plane routes reconnect/destroy to the right backend. */ provider: string; /** Absolute mount path of the workspace root inside the remote env; remote paths normalize against this. */ mountPath: string; /** Set when this env was produced by resuming a snapshot ({@link RemoteExecutionEnv.resumeVM}), else undefined. */ snapshotId?: SnapshotId; /** Token for {@link RemoteExecutionEnv.reconnect} when the VM is still running (vs suspended to a snapshot). */ sessionToken?: SessionToken; /** How a checkpointed workspace is restored on resume (1.257.2 hardening, codex review of [500]②): * `"park_only"` = durable-park-only suspend degrade (non-suspendable env; workspace persists on the * target, resume skips `resumeVM`). Absent = legacy/snapshot handle — resume treats a MISSING * `snapshotId` as corruption (fail-closed) unless the resumed env is itself non-suspendable * (tolerance for park handles minted by 1.257.1 before this field existed). */ restoreMode?: "park_only"; } /** * A reference to a secret injected at {@link RemoteExecutionEnv.connect}/{@link RemoteExecutionEnv.postResumeInit} * time (design/48 §5 gap 7) — never baked into the image. The control plane resolves {@link ref} to a value out of * band; only the reference travels through core. */ export interface SecretRef { /** Logical name the secret is exposed under (an env-var name in the remote env for `destination:"remote"`). */ name: string; /** Opaque locator the control plane resolves to a value (vault path, KMS key id, …). */ ref: string; /** * Where the resolved secret is consumed (design/61 §9 B, council). Default `"remote"` (the original * semantics): injected **into the remote env** as `name` (E2B env-var) — visible to code running there. * `"adapter"`: used by the **adapter itself** to establish/authenticate the connection (an SSH private key, * an ADB token) and **MUST NOT be injected into the target machine/device** — a credential that reaches an * un-isolated target's environment is the lethal trifecta (design/53 §C). An adapter MUST honor this. */ destination?: "remote" | "adapter"; } /** Configuration for {@link RemoteExecutionEnv.connect}. */ export interface RemoteConnectConfig { /** Resume from this snapshot instead of provisioning a fresh workspace. */ snapshotId?: SnapshotId; /** Secrets to inject at connect time (design/48 §5 gap 7). */ secrets?: SecretRef[]; /** Abort provisioning/connection. */ abortSignal?: AbortSignal; } /** * Options for the VM-lifecycle ops {@link RemoteExecutionEnv.suspendVM} / {@link RemoteExecutionEnv.resumeVM} * (design/49 v1.5, code-ready council BUG#2). Parity with {@link RemoteConnectConfig.abortSignal} / * {@link ExecStreamOptions.signal}: without an abort hook a hung provider pause/restore (network partition) * would pin the calling worker forever. The implementation races the op against this signal + an internal * timeout; on abort it returns `{ok:false}` with code `"aborted"`. */ export interface VmLifecycleOptions { /** Abort a hung pause/restore. */ abortSignal?: AbortSignal; } /** * One chunk streamed from {@link RemoteExecutionEnv.execStream} (design/48 §5 gap 3, council BUG#3). A run is a * sequence of `stdout`/`stderr` chunks terminated by exactly one `exit` chunk carrying the typed exit code. * * The async iterator THROWS a {@link RemoteExecutionError} if the stream fails before an `exit` chunk arrives * (transport loss, or a {@link RemoteExecutionEnv.suspendVM} that aborts the in-flight command) — that is the * streaming analogue of the base {@link ExecutionEnv}'s never-throw `Result` contract, and it guarantees a * consumer is never left awaiting a chunk that will never come. */ export type OutputChunk = { type: "stdout"; data: string; } | { type: "stderr"; data: string; } | { type: "exit"; exitCode: number; }; /** Options for {@link RemoteExecutionEnv.execStream}. */ export interface ExecStreamOptions { /** * Isolation routing hint (design/48 §2; renamed from `tier` per §11 #10). The *classification* that decides * a command's tier — static tool table + Tier1-safe sub-command allow-list, fail-closed to Tier 2 — is the * env/control-plane's job and is DEFERRED; this param only lets a caller pass an explicit route when known. */ sandboxTier?: SandboxTier; /** Working directory; relative paths resolve against the workspace root. */ cwd?: string; /** Extra environment variables for the command. */ env?: Record; /** * Total command wall-clock timeout in seconds. **🔴 Per-command — independent of the env/sandbox lifetime.** * A remote adapter MUST NOT fall back to the sandbox lifetime when this is unset (service [45]: an E2B adapter * that defaulted a missing per-command timeout to `cfg.timeoutMs` = the sandbox lifetime made a seconds-long * command wait the *whole sandbox lifetime*, ~30 min, on a provider RPC hang). "No `timeout`" means "no * wall-clock cap on a *making-progress* command" — NOT "wait until the env dies"; liveness is still bounded by * {@link readTimeoutMs} (see below). Distinct concerns: `timeout` caps a slow-but-progressing command; * `readTimeoutMs` catches a *hung/unreachable* one. */ timeout?: number; /** Abort the command. Aborting ends the stream with a {@link RemoteExecutionError} (code `"aborted"`, no `exit` chunk). */ signal?: AbortSignal; /** * Idle/liveness read timeout (ms). If no {@link OutputChunk} arrives within this window, `execStream` ends * with a {@link RemoteExecutionError} (code `"timeout"`) instead of hanging. **Needed in practice for remote * backends** (design/48 §5, service [7]): E2B issue #1128 — a *streaming* call sets no read timeout (only unary * does), so an unreachable sandbox hangs the worker forever. Distinct from `timeout` (total wall-clock). * * 🔴 **Liveness contract (service [45]/[59]):** a remote adapter SHOULD enforce a default liveness bound even * when this is unset (a missing idle timeout MUST NOT mean "wait forever / until the env dies"), and that bound * MUST cover the **command-creation/handshake RPC**, not only the post-first-chunk loop — a provider hang * typically occurs at creation, before any chunk arrives, so an idle timer started only after the first chunk * misses it. On trigger, throw {@link RemoteExecutionError} with code `"timeout"` (retryable by the caller per * the command's idempotency — the adapter MUST NOT blind-retry). */ readTimeoutMs?: number; /** Per-call output cap (design/48 §5 gap 8): stop streaming after this many bytes to avoid a `find /` flooding the control plane. */ maxOutputBytes?: number; } /** Stable error codes for the remote seam's lifecycle/streaming operations (design/48 §5/§11 BUG#2). */ export type RemoteExecutionErrorCode = /** A lifecycle/exec op failed because the workspace is (or became) suspended. */ "suspended" /** {@link RemoteExecutionEnv.suspendVM} refused because a command was still in flight (council #4). */ | "command_in_flight" /** Connect/resume/reconnect failed to reach or provision the remote workspace. */ | "connect_failed" /** A post-resume consistency step failed (design/48 §5/#8) — caller must destroy the env. */ | "post_resume_failed" /** The command/stream was aborted via its `AbortSignal`. */ | "aborted" /** * A command's wall-clock {@link ExecStreamOptions.timeout} or liveness {@link ExecStreamOptions.readTimeoutMs} * was exceeded — including a provider RPC hang at command creation (service [45]/[59]). **Retryable** by the * caller per the command's idempotency (the adapter MUST NOT blind-retry). Distinct from `"aborted"` (a * caller-driven `AbortSignal`) and `"connect_failed"` (workspace provisioning, not a per-command hang). */ | "timeout" /** * The op is not supported by this adapter's capabilities (design/61 §9 A) — e.g. `suspendVM`/`resumeVM` on a * non-suspendable SSH/ADB env. Permanent for this env; check {@link RemoteExecutionEnv.capabilities} first. */ | "unsupported" /** * A **transient** authentication step that is expected to succeed on a retry (design/61 §9 C) — e.g. an ADB * device awaiting first-time authorization (connect once → user authorizes → connect again succeeds). * **Retryable** (typically retry-exactly-once). Distinct from {@link "auth_failed"} so a real rejection isn't * retried and a bounded ADB retry doesn't false-kill an SSH transient. */ | "auth_transient" /** * **Permanent** authentication failure (design/61 §9 C) — wrong/rejected SSH key, revoked token. **NEVER * retry** (retrying burns the whole attempt budget and can lock accounts). The caller must surface it. */ | "auth_failed" /** * The transport connection dropped mid-session (TCP RST, network partition, peer close) — design/61 §9 C/#14. * **Retryable** by re-establishing the connection (idempotent for a persistent SSH/ADB session). An adapter * MUST map raw transport errors (DNS `ENOTFOUND`, TCP RST) onto a typed code, not leave them as `"unknown"`, * else a retry whitelist misses them. */ | "transport_lost" /** Unclassified provider/transport failure. */ | "unknown"; /** * Error surfaced by {@link RemoteExecutionEnv} lifecycle ops and by {@link RemoteExecutionEnv.execStream}'s * iterator. Mirrors the vendored `ExecutionError` shape but carries the remote-specific code set; it is kept * separate from the vendored `ExecutionErrorCode` union deliberately — those codes only need to widen once the * design/45 suspend/resume *runtime* lands and the Runner/orchestrator layer must thread `"suspended"`. */ export declare class RemoteExecutionError extends Error { readonly code: RemoteExecutionErrorCode; constructor(code: RemoteExecutionErrorCode, message: string, cause?: Error); } /** * A remote, stateful, suspendable {@link ExecutionEnv} (design/48 §5). Extends the base with workspace identity, * connection lifecycle, streaming exec, and post-resume reconciliation. Every method follows the base contract: * **never throw/reject** — encode failures in the returned `Result` (the one exception is {@link execStream}, * whose iterator may throw mid-stream; see {@link OutputChunk}). * * Lifecycle methods are typed but their RUNTIME wiring (task suspension, `Checkpoint` persistence of the * {@link WorkspaceHandle}, cross-replica resume) is implemented with design/45 — see the file header. * * 🔴 **Universal liveness contract (service [45]/[59] class-fix) — applies to EVERY remote RPC**, not just * `execStream`: the inherited {@link ExecutionEnv} filesystem ops (`readTextFile`/`writeFile`/`listDir`/…), the * streaming/buffered exec, AND the VM-lifecycle ops below (`connect`/`reconnect`/`suspendVM`/`resumeVM`/ * `postResumeInit`/`destroy`). Each is a network round-trip to a provider that can hang. The contract: * 1. **Every remote RPC SHOULD enforce a bounded default liveness/idle timeout** even when no `abortSignal`/ * timeout is given — a hung provider call MUST NOT wait until the env/sandbox lifetime expires (the * service [45] bug, but generalized: it was found on `exec`, and `files.read`/`resumeVM`/etc. share the * exact exposure). An optional `abortSignal` (where present) composes with, but does not replace, this default. * 2. **🔴 Bound on LIVENESS (no progress), not a fixed short wall-clock** — distinguish *hung* from *slow but * progressing* (search [60] / service [50] data-transfer sharpening). A **data-transfer** op — a large-workspace * `suspendVM` snapshot, a big `writeFile`/`readTextFile` — may legitimately run for minutes; a fixed short * wall-clock would false-kill the (durable-checkpoint *命门*) path. So such ops bound on an **idle/no-heartbeat** * window (reset by observable progress), not total elapsed time. Cheap control RPCs (`isRunning`, a small * `connect` handshake) may use a short wall-clock. The adapter sizes the bound per op class. * 3. **A liveness breach surfaces as a typed, retryable error**: `RemoteExecutionError` code `"timeout"` for the * lifecycle/stream ops; for the inherited filesystem ops (which return the vendored `Result<_, FileError>`), * a `FileError` — but still **bounded, never an unbounded wait**. Retry is the caller's decision per the op's * idempotency; the adapter MUST NOT blind-retry. */ export interface RemoteExecutionEnv extends ExecutionEnv { /** * What this adapter can actually do (design/61 §9 A, council). The seam was built for E2B (a full * `{isolation:true, suspendable:true}` adapter); **partial-capability** adapters (SSH/ADB: `{false, false}` * — a real machine/device, not snapshotable and not isolated) declare it here. **Required + explicit** so * the orchestrator never has to guess: there is no safe default (assuming isolation when there is none is a * security hole; assuming suspendable crashes on `suspendVM`). Use {@link isSuspendable}/{@link isIsolated} * rather than the structural {@link isRemoteExecutionEnv} (method presence ≠ semantics — a stub `suspendVM` * that returns `"unsupported"` still satisfies the structural check). Extensible: add fields as new * partial-capability classes appear. */ readonly capabilities: { /** True iff actions are contained (E2B microVM); false for a real target (SSH host / ADB device). The * orchestrator tightens autonomy + the design/37 policy gate when this is false (design/53 zero-trust). */ isolation: boolean; /** True iff the env can be snapshot/`suspendVM`'d (E2B); false for SSH/ADB (durable suspend N/A). * * ⚠️ LOAD-BEARING INVARIANT (opus review 1.257.2): `suspendable:false` on a remote env also asserts * the workspace is EXTERNALLY DURABLE — it persists on the target across the env object's lifetime * (true for SSH hosts / ADB devices). The durable-park-only suspend degrade (service [500]②) rests * on this: it skips `suspendVM` and trusts the factory to reconnect to the SAME workspace on resume. * An adapter for a non-suspendable EPHEMERAL backend (e.g. a snapshot-less container torn down with * the transport) must NOT be modeled as `suspendable:false` remote — it would silently take the * park-only branch and resume onto a fresh empty workspace. Model such a backend as a per-task env * without durable suspend instead (the human gate refuses it), or extend capabilities with an * explicit workspace-durability flag before building one. */ suspendable: boolean; }; /** Identity of the connected workspace (design/48 §5 gap 1). Synchronous: it is data the env already holds. */ workspaceHandle(): WorkspaceHandle; /** Provision/attach the remote workspace and inject secrets. Returns the resulting {@link WorkspaceHandle}. */ connect(config?: RemoteConnectConfig): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; /** * Snapshot the workspace and (provider permitting) stop billing; returns the {@link SnapshotId} to resume from. * * **Council #4 — must not leave an in-flight command hung.** If a {@link Shell.exec}/{@link execStream} is in * flight, an implementation MUST either (a) refuse with `RemoteExecutionError("command_in_flight")`, or (b) * abort the in-flight command — buffered `exec` then resolves an `Err` and an in-flight {@link execStream} * iterator throws `RemoteExecutionError("suspended")`. It must NOT silently snapshot and leave the command's * promise/iterator pending forever. * * **🔴 Atomicity contract (design/49 v1.5, code-ready council BUG#1/#13).** This is the FIRST durable side * effect of a remote durable-suspend, so its all-or-nothing semantics are load-bearing for the Runner's * commit ordering: * - `{ok:true}` ⇒ the VM is paused at `value` (a `SnapshotId`); the caller may now persist a checkpoint. * - `{ok:false}` ⇒ the VM is left in its ORIGINAL (running) state, untouched — the caller can safely fall * back to the synchronous `onAsk` gate as if no suspend was attempted (Runner: design/49 §4①). * An implementation MUST NOT return `{ok:false}` after it has already paused/snapshotted the VM. The gate * path means no command is in flight (the batch's prior calls are awaited, the gated call has not run), so * `command_in_flight` should not arise; if it does, the Runner treats it as an ordinary `{ok:false}` and * falls back to `onAsk` — see {@link RemoteExecutionErrorCode}. */ suspendVM(options?: VmLifecycleOptions): Promise<{ ok: true; value: SnapshotId; } | { ok: false; error: RemoteExecutionError; }>; /** Restore the workspace from a snapshot. Idempotent + re-entrant (a replica that crashed mid-suspend can be * superseded by another resuming the same {@link SnapshotId} without corruption — aligns with design/45 resolve-CAS). * Pass `options.abortSignal` so a hung restore cannot pin the resuming worker (design/49 BUG#2). */ resumeVM(snapshotId: SnapshotId, options?: VmLifecycleOptions): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; /** Re-attach the control-plane transport to a still-running VM after a control-plane reconnect (design/48 §5 gap 4). */ reconnect(sessionToken: SessionToken): Promise<{ ok: true; value: WorkspaceHandle; } | { ok: false; error: RemoteExecutionError; }>; /** * Re-establish consistency AFTER {@link resumeVM}/{@link reconnect} (design/48 §5 gap, council #8): re-fetch git * remote refs + invalidate stale package/index caches, and re-inject secrets (short-lived credentials may have * expired while suspended). Any failed step → `Err("post_resume_failed")` and the caller MUST destroy the env. * * Note: a resumed guest's network/long-lived connections (git remote / API / registry) are NOT guaranteed to * survive the snapshot (clay decision 2), so this is also where the VM-internal agent rebuilds those. * * 🔴 Ordering red line (design/48 §5/#6): at-rest encryption of the memory snapshot must be ensured BEFORE * secrets are injected — never let plaintext credentials land in an unencrypted snapshot. That encryption is a * service-side property of the snapshot store; this method must fail-and-clean if it cannot be guaranteed. */ postResumeInit(): Promise<{ ok: true; value: void; } | { ok: false; error: RemoteExecutionError; }>; /** * Stream a command's output (design/48 §5 gap 3) — the standard path for long build/test runs. Distinct from * the buffered base `exec()` (council #3: no overload). Yields `stdout`/`stderr` chunks then exactly one `exit` * chunk; the iterator throws {@link RemoteExecutionError} if the stream fails before `exit` (see {@link OutputChunk}). * * 🔴 **Timeout/liveness contract (service [45]/[59]) — applies to BOTH `execStream` and the base `exec`:** a * per-command timeout is **independent of the env/sandbox lifetime** and an adapter MUST NOT fall back to the * lifetime when none is given (else a hung provider RPC pins the worker for the whole sandbox lifetime). An * adapter SHOULD enforce a bounded default liveness/idle timeout that also covers the command-creation RPC, and * surface a liveness/timeout breach as a typed, retryable {@link RemoteExecutionError} code `"timeout"`. See * {@link ExecStreamOptions.timeout} / {@link ExecStreamOptions.readTimeoutMs}. */ execStream(command: string, options?: ExecStreamOptions): AsyncIterable; /** * Tear down the workspace and release provider resources. Best-effort; must never throw (like `cleanup`). * **Must be idempotent** — safe to call more than once (the second call is a no-op). The Runner calls it * once on task end, and `prepareTask` calls it on a prepare-time throw; a remote impl may also be reaped, * so a defensive double-call must not error or double-bill. * * 飞轮 [519] contract note: on a NON-isolated env (no `capabilities.isolation` — host lane, SSH host), * destroy() is workspace/object-level cleanup and must NOT reap still-running background processes — * that is `disposeBackgroundShells`' job, which honours the timeout/session keep-alive except-list * ([511]③ monitor timeout anchor). An isolated env (container/VM) naturally takes everything down; * the Runner's envDying settle accounts for that with an accurate killed receipt beforehand. */ destroy(): Promise; } /** Context handed to an {@link ExecutionEnvFactory} for each task (design/48 §5 / §7 Q7). Deliberately minimal: * per-task identity is enough to allocate/route a per-task container; richer routing (tenant/principal) is * captured in the factory closure by the trusted control plane that builds it. */ export interface ExecutionEnvFactoryContext { /** Resolved session id for the task — the stable identity of its per-task workspace. */ sessionId: string; /** Caller-supplied task id, when set on the `TaskSpec`. */ taskId?: string; /** * design/97 CORE-6 — per-task ISOLATION hint threaded from the workflow's `ctx.agent({ isolation })` via the * TRUSTED `RunInternals` channel (never from the untrusted `TaskSpec`). When `"worktree"`, a control-plane * factory should mint a git-worktree-rooted env for this agent (e.g. via {@link addWorktree}) WITH a * `destroy()` that removes the worktree, and return the SHARED base env (no `destroy`) otherwise. Unset = * default (the factory's normal per-task env). */ isolation?: "worktree"; /** * Blackboard 2026-07-03 (clay dogfood — sub-agents landing in an EMPTY sandbox): the PARENT task's * effective working root, threaded (like `isolation`) via the TRUSTED `RunInternals` channel when this * task is a sub-agent (workflow `ctx.agent` / Task delegation). CC parity: a Task sub-agent inherits the * main session's cwd. A single-user/TOC factory SHOULD root the child env here (unless `isolation` * requests a worktree — that wins); a multi-tenant/TOB factory minting isolated containers may ignore * it. Absent on top-level tasks. Static shared-`executionEnv` deployments need nothing — the child * already shares the parent env (and its cwd). */ parentCwd?: string; } /** * A **trusted control-plane** factory that mints a per-task {@link ExecutionEnv} (design/48 §5 answers core * gap-a / §7 Q7). Lives on `RunnerDeps` (deployment-level) — NOT on `TaskSpec` — so an untrusted caller can * never inject its own env and escape the sandbox (design/44 §7 Q4 red line; clay decision 3: the Docker * fast-lane must be control-plane-assigned, never task-selectable). * * The remote model is "one container per task", so the factory is invoked once per task. The Runner owns the * lifetime of a factory-produced env: if it implements {@link RemoteExecutionEnv.destroy} (see {@link hasDestroy}), * the Runner calls it when the task ends. */ export type ExecutionEnvFactory = (ctx: ExecutionEnvFactoryContext) => ExecutionEnv | Promise; /** True when `env` exposes a lifecycle {@link RemoteExecutionEnv.destroy} the Runner should call on task end. */ export declare function hasDestroy(env: ExecutionEnv): env is ExecutionEnv & Pick; /** Structural check that `env` implements the remote seam (lifecycle + streaming surface + declared capabilities). */ export declare function isRemoteExecutionEnv(env: ExecutionEnv): env is RemoteExecutionEnv; /** * Can this env be durably suspended/snapshotted (design/61 §9 A)? Use this — NOT the structural * {@link isRemoteExecutionEnv} — before calling `suspendVM` (a non-suspendable SSH/ADB env declares * `capabilities.suspendable: false` but still has the method, which returns `"unsupported"`). Non-remote * (in-process) envs are not suspendable. * * A composite type guard (structural AND capability) so a `suspendVM` call site narrowed by THIS predicate * gets the {@link RemoteExecutionEnv} type without a cast — `true` always implies the structural check too. */ export declare function isSuspendable(env: ExecutionEnv): env is RemoteExecutionEnv; /** * Is this env an isolated sandbox (design/61 §9 A, design/53)? `false` for a real SSH host / ADB device AND * for any in-process env (the host process is not isolated). The orchestrator tightens autonomy + the * design/37 policy gate when this is `false` (zero-trust on an un-isolated target). The safe default is * `false` — isolation must be explicitly declared, never assumed. */ export declare function isIsolated(env: ExecutionEnv): boolean; //# sourceMappingURL=remote-env.d.ts.map