Back to Fresh

Remote Authority & Workspace Trust

docs/internal/remote-authority-trust.md

0.4.334.9 KB
Original Source

Remote Authority & Workspace Trust

Purpose: the one map of where the editor acts (the Authority backend slot — local / SSH / docker-exec / kubectl-exec), how a remote backend is built, kept alive, and reconnected, and whether code may run there (Workspace Trust + the live env provider). Covers what is implemented today versus what is forward-looking design.

This doc is the as-built companion to the Authority abstraction design, the per-session-backends design, the K8s authority design, and the trust/env design set. Where those describe direction, this records what the tree actually does.


1. The Authority pattern

1.1 What it is

Authority is the single backend slot answering "where does this primitive run?" — file I/O, the integrated terminal, plugin spawnProcess, LSP server spawn, formatters, find-in-files, save, recovery, the file watcher. Everything routes through the active authority; nothing in core branches on "is this SSH / a container."

The struct carries:

  • filesystem: Arc<dyn FileSystem + Send + Sync> — local StdFileSystem or RemoteFileSystem.
  • process_spawner: Arc<dyn ProcessSpawner> — one-shot "run and collect" (plugin spawn, git, find-in-files).
  • long_running_spawner: Arc<dyn LongRunningSpawner> — long-lived stdio children (LSP servers; future tool agents). Routing LSP through here is what gives a container authority in-container LSP with no special-case in LspHandle.
  • terminal_wrapper: TerminalWrapper — how the integrated terminal is launched.
  • display_label: String — status-bar/explorer label (empty = render nothing; SSH leaves it empty and lets the filesystem's remote_connection_info() annotate disconnect).
  • path_translation: Option<PathTranslation> — host↔remote workspace path mapping (container only).
  • workspace_trust: Arc<WorkspaceTrust> — mandatory execution gate, shared with every spawner.
  • env_provider: Arc<EnvProvider> — live activated env, shared with every spawner.
  • command_wrap: CommandWrap — how an interactive agent argv is composed to run inside the backend.

Constructors (all unit-testable in isolation): Authority::local, Authority::ssh, Authority::kube / kube_from_connection, Authority::from_plugin_payload.

1.2 Opaque-to-core, sole-router (load-bearing principles)

The four routing fields are the entire contract. The only code that names a backend by string ("docker", "ssh", "kubectl") is that backend's constructor and its spawner module; consumers never inspect the kind. This is enforced socially (the opaque-to-core principles in the Authority design) and partly by the type system — see §1.5.

1.3 Authority is not Clone, owned per Window

Authority deliberately does not implement Clone. It is moved between slots, never copied. A session's backend/trust/env therefore cannot leak into another window — the isolation is a compile-time fact, not a runtime check. The per-window field already exists: WindowResources holds an authority, exposed via Window::authority().

1.4 Transitions are atomic and destructive (production path)

Installing a new authority does not swap fields in place. Editor::install_authority(new) stashes the replacement in pending_authority and calls request_restart; the standalone entry point or EditorServer::rebuild_editor (daemon) drops the entire Editor, rebuilds it, and reinstalls the new authority via set_boot_authority before plugins load. Every cached Arc<dyn FileSystem>, LSP handle, terminal PTY, and in-flight task dies with the old editor.

Rationale: an in-place swap would require enumerating every cache that closed over the old filesystem/spawner (open buffers, FsManager, Quick Open's FileProvider, LspManager, TerminalManager, the watcher, recovery, background tokio tasks). A miss manifests as "files save to the wrong place" — a trust-destroying bug class. The restart path already drops and rebuilds everything correctly, at a cost paid once per attach/detach (never per keystroke). The escape hatch to a no-restart swap lives at install_authority.

change_working_dir uses the same machinery to switch project roots — authority swap and project-root change are the same main-loop primitive (drop + rebuild) with different "what changed" semantics.

Daemon (session mode): fresh --session / fresh server runs a long-lived EditorServer. It must not exit on a transition or clients disconnect, so rebuild_editor mirrors the standalone restart: save workspace, drop the editor, swap current_authority and/or working_dir, rebuild, restore buffers, repaint clients. EditorServerConfig has a startup_authority slot and an opaque session-keepalive slot (SSH backs this with the runtime + SshConnection + reconnect task; dropping any one tears the remote session down).

1.5 Per-session activation primitive (partly landed)

A no-restart, single-window swap primitive has landed: Editor::set_session_authority(window_id, authority) swaps one window's authority and re-points that window's LSP, mirroring into the editor-wide cache only when it is the active window. Covered by e2e tests.

Still gated (so production attach still uses the destructive restart): live multi-session (the active session is pinned to the first window), per-window keepalives so a background window keeps its live backend, and cache-invalidation of buffers/terminals opened under the old authority. The forward-looking target ("one authority per Session, exactly one active; background sessions hold dormant authorities") is direction, not fully shipped.

1.6 SessionScope — minting trust+env together

SessionScope { trust, env } is the one blessed way to obtain per-session trust + env. SessionScope::for_root(root, project_state_dir) mints fresh handles owned by one session: a per-root WorkspaceTrust adopting that project's recorded level, and a per-session EnvProvider that restores a previously-activated recipe only when trusted. Move-only, consumed by Authority::local_scoped. This is what prevents "trust project A" from raising the live level project B's spawns are gated against.


2. CommandWrap and TerminalWrapper — composing into the backend

Two related seams decide how a command runs inside the active backend.

TerminalWrapper launches the integrated shell:

  • host_shell() — detected shell, no args, manages_cwd = false (the terminal manager sets cwd itself).
  • ssh(params, remote_dir)ssh -t … 'cd <dir>; exec $SHELL -l', manages_cwd = true.
  • kube(target, base_env)kubectl exec -it … -- sh -lc 'export …; cd <ws>; exec $SHELL -l', manages_cwd = true.

A re-parenting wrapper pins cwd through its own args, so manages_cwd = true; with_user_shell_override is then a no-op for it (replacing the command would drop the re-parenting and spawn the user's shell on the host). Without the SSH/kube wrappers the embedded terminal silently ran on the local machine — that was the bug they fix.

CommandWrap generalizes this to an arbitrary interactive argv (e.g. claude --resume <id> from the Orchestrator agent-resume path). Authority::terminal_command(argv) dispatches:

  • Direct (local) — argv is the PTY child; terminal sets cwd.
  • Prefix(vec) (container) — docker exec -it [-u][-w][-e] <id> <argv>, argv appended verbatim (never a shell string), cwd pinned by -w.
  • Ssh { params, remote_dir }ssh has no cwd flag, so a cd <dir>; exec <argv> shell hop, argv POSIX-quoted.
  • Kube { target, base_env } — same shell-hop, env exported first.

This composition seam is shipped for local, container, SSH, and Kubernetes; dedicated SSH/kube agent argv builders compose the interactive terminal command for each carrier.


3. The remote agent stack (SSH and Kubernetes)

SSH and Kubernetes share almost the entire remote stack; only the carrier differs. This is the load-bearing thesis of the K8s authority design: the K8s authority is the SSH authority with the transport swapped from ssh to kubectl exec. Everything above the channel is reused verbatim. Both are shipped (the K8s/kubectl-exec slice landed first as EKS, then generalized to any cluster).

3.1 The Python agent

A single embedded Python agent file, included at compile time as AGENT_SOURCE. The carrier streams it to a remote python3 and the agent then reads JSON-lines protocol messages off stdin. Zero install — only python3 is required on the far side (a missing-interpreter exit code from the bootstrap is detected and surfaced as "Python 3 was not found on the remote host…").

Bootstrap framing is byte-count based — import sys;exec(sys.stdin.read(<agent byte length>)) — so it needs no remote shell, here-doc, or bash.

3.2 Protocol & channel

JSON-lines, short keys to save bandwidth: request {id, m, p}, response {id, d?, r?, e?}. A protocol version is checked in the ready handshake; mismatch is a hard error. Binary is base64.

AgentChannel multiplexes request/response over the carrier's stdin/stdout:

  • Long-lived read and write tasks survive transport drops (from_transport).
  • request/request_streaming register a pending entry keyed by id; streaming uses send().await for backpressure (a prior try_send silently dropped data).
  • A bounded default request timeout; a timed-out request marks the channel disconnected so the reconnect path engages (a remote drop never freezes the UI forever).
  • A stable per-channel id survives hot-swaps — a durable key mapping a reconnect event back to its window.
  • replace_transport hot-swaps the reader/writer in place (write task first, then read task drains stale pending requests before flipping connected true) and fires reconnect_notify.

3.3 Carriers and connections

  • SSH: SshConnection::connect spawns ssh -o StrictHostKeyChecking=accept-new [-p][-i] <extra_args> user@host <python bootstrap>. stderr is piped, not inherited — an inherited stderr would let ssh scribble "Could not resolve hostname" straight over the ratatui alternate screen; instead the most-specific stderr line is folded into the connection error (ssh_eof_error). kill_on_drop(true) covers a connect future cancelled mid-handshake. ConnectionParams::parse accepts host, user@host, user@host:port, and a leading ssh:// (user optional).
  • Kubernetes: the RemoteTransport trait supplies a configured carrier Command; KubectlExecTransport builds kubectl [--context] exec -i -n NS [-c C] POD -- python3 -u -c <bootstrap>. KubeConnection::connect bootstraps the same agent, discards carrier stderr (same alternate-screen reason), and starts the heartbeat. KubeTarget is {context?, namespace, pod, container?, workspace?}; display() yields k8s:ctx/ns/pod[/container].

bootstrap_agent is the shared "spawn carrier, stream agent, await ready, check version" function for the transport seam; SSH keeps its own near-identical establish_ssh_transport (the transport seam was added additively so SSH stayed byte-for-byte unchanged — SSH may migrate onto it later).

3.4 RemoteFileSystem

RemoteFileSystem implements the synchronous FileSystem trait over the channel via request_blocking (the channel owns a runtime handle). Notable choices:

  • read_file/read_range stream base64 chunks; read_range validates the exact byte count (matches local read_exact; a short read signals truncation/race).
  • walk_files is a server-side walk: the agent walks the tree and streams batches of relative paths, processed as they arrive so memory stays bounded; dropping the receivers cancels the server.
  • write_patched sends Copy{offset,len} / Insert{data} ops so a save is O(edits), not O(file) — the agent reassembles. Append and truncate are server-side atomic ops.
  • Errors map remote strings back to io::ErrorKind (NotFound, PermissionDenied, …).
  • The remote-specific FileSystem hooks — remote_connection_info, is_remote_connected, remote_channel_id, remote_reconnect_notify — are how core observes connection state without knowing the backend kind.

SlowFileSystem is a test-only decorator that injects per-op latency and counts calls (presets for slow-network / slow-disk), used to validate the editor never blocks the UI on slow I/O.

3.5 The spawners

Three spawner families implement ProcessSpawner (one-shot) and LongRunningSpawner (stdio), all gated by trust and carrying the env provider:

  • Local: LocalProcessSpawner / LocalLongRunningSpawner run children directly via tokio, applying the captured env, honoring ProcessLimits (cgroups/rlimits) on local children. spawn_cancellable / spawn_to_file stream stdout to disk for huge outputs (e.g. a multi-megabyte git show).
  • SSH: RemoteProcessSpawner routes one-shots through the agent channel's exec; RemoteLongRunningSpawner opens a separate ssh … <remote-cmd> per LSP server whose piped stdio is the remote process's stdio. A separate connection per server (rather than multiplexing through the agent) is a deliberate trade-off: the agent's one-shot exec can't keep a process alive with writable stdin, and abstracting the whole LSP I/O layer over the channel would be a far larger change. StdioChild carries spawned_locally = false so host-side resource limits skip themselves (the local PID is the ssh client, not the server).
  • docker-exec: DockerExecSpawner / DockerLongRunningSpawner run docker exec [-i] [-u][-w][-e KEY=VAL] <id> <cmd>. base_env carries the container's captured userEnvProbe (notably PATH) injected on every exec, so a pylsp installed by postCreateCommand into ~/.local/bin is actually discoverable. command_exists runs command -v inside the container so the LSP popup reflects the container's PATH, not the host's.
  • kubectl-exec: KubectlLongRunningSpawner runs kubectl exec -i … -- sh -c 'cd <dir>; exec env K=V… "$0" "$@"' <cmd> <args>. kubectl exec has no -w/-e flags, so cwd and env are injected via the sh -c wrapper. Uses -c (not -lc) since env is replayed explicitly — avoids profile-sourcing latency/noise on the server's stdout. One-shot commands and the filesystem ride the agent channel (RemoteProcessSpawner/RemoteFileSystem); only the long-running path needs this separate carrier.

Env injection on backends that pass an argv array (SSH/docker/kube) uses env K=V … cmd args rather than a shell string (env_wrap); the SSH login-shell path uses exec env … cmd with POSIX shell-quoting (build_remote_exec).

3.6 Keepalive, heartbeat, and reconnection (all shipped)

A remote authority needs three live resources kept alive across the attach-time editor rebuild, owned in a keepalive bundle (SshKeepalive, KubeKeepalive): the carrier connection, the reconnect task, and a dedicated tokio runtime. The runtime is load-bearing — the agent channel's read/write tasks must not ride the editor's per-instance runtime, which is dropped during the attach restart; if they did, every file op would fail with "Channel closed" the instant the attach completed. connect_ssh_authority / connect_kube_authority bootstrap on a short-lived helper thread (because block_on can't run inside the caller's async context), hand back the live runtime, and park it in the keepalive. Both race the connect against an optional cancel signal so a hung handshake leaves no orphan child.

Heartbeat: a periodic info ping keeps an idle agent stream warm against ELB/NAT idle timeouts (on the order of minutes) that would otherwise silently drop the connection — the client never sees a FIN, so the next request just hangs. Holds only a Weak ref, so it self-terminates when the channel is dropped. info is handled by every agent version → no protocol bump. Shipped for both carriers.

Reconnect: a background task watches channel.is_connected(); on a drop it retries on a fixed interval, re-establishing the carrier and calling replace_transport to hot-swap. SSH re-runs establish_ssh_transport; K8s re-runs kubectl exec against the same target. Caveat: a pod reschedule/eviction changes the pod name, which the same-target reconnect does not re-resolve — the "resolve current pod" plugin callback is an open item (planned). Same-name reconnect still covers the common transient-drop / idle-blip case.

Respawn embedded terminals on silent reconnect (shipped): a hot-swap revives the agent channel, but an embedded terminal opened its own carrier (a separate ssh -t / kubectl exec PTY) which died with the old connection. replace_transport fires reconnect_notify; the editor forwards each notification to AsyncMessage::RemoteReconnected (event-driven, not polled). Editor::detect_remote_terminal_reconnects (a per-frame edge detector) is gated on a live remote authority (remote_connection_info().is_some() && !is_remote_connected()) rather than on the persisted spec kind — so plain fresh ssh://… windows revive too, not only Orchestrator-attached ones — and respawn_terminals_through_authority returns a revive count to avoid double-respawn. Covered by e2e tests.


4. Plugin-built backends, persistence, and reconnection specs

4.1 Plugin payloads

Three small plugin ops: editor.setAuthority(payload), editor.clearAuthority(), editor.spawnHostProcess(...) (runs on the host regardless of the active authority — needed by a plugin to run devcontainer up before the authority it wants exists). setAuthority is fire-and-forget: the editor restarts before any follow-up code on its return could run.

AuthorityPayload is a tagged, additive shape: filesystem (currently only Local — containers bind-mount, so paths coincide), spawner (Local or DockerExec { container_id, user?, workspace?, env }), terminal_wrapper (HostShell or Explicit), display_label, and optional path_translation. Authority::from_plugin_payload is the only place "kind + params" becomes concrete Arc<dyn …>. serde's tagged-enum representation means old payloads keep parsing as new kinds are added.

Env is deliberately not expressed in SpawnerSpec — it is a live provider set via setEnv/clearEnv, because a serializable setAuthority payload cannot carry the live SSH AgentChannel. SpawnerSpec is for backend selection only.

4.2 Path translation (container only)

PathTranslation { host_root, remote_root } maps host-side buffer paths to their in-container mount path and back (host_to_remote / remote_to_host; None for paths outside the workspace, e.g. system headers). The primary consumer is LSP URIs: editor buffers are host-side, but the LSP server is across the mount and only knows the remote-side path. Translation happens at the URI boundary, so the editor keeps host paths internally and the server sees the paths it expects. Local and SSH leave this unset — they operate directly in one path space.

4.3 SessionAuthoritySpec — the persisted, rebuildable descriptor

SessionAuthoritySpec is the declarative, source-of-truth counterpart to the live (non-serializable) Authority, persisted in the per-dir workspace file so a backend survives an editor restart instead of degrading to local. Variants: Local, Plugin(AuthorityPayload) (devcontainer — only the owning plugin can re-run devcontainer up), RemoteAgent(RemoteAgentSpec) (SSH/Kubernetes — reconnectable from core). Authority::session_spec() derives the spec from the live authority's command_wrap, so a plain fresh ssh://… launch carries a real RemoteAgent spec (making persistence, the dormancy model, and manual reconnect all work) rather than the historical inert Local default. RemoteTransportSpec is Ssh{user?,host,port?,identity_file?,remote_path?,extra_args} or KubectlExec{context?,namespace,pod,container?,workspace?}; RemoteAgentSpec also carries base_env, plus window/label/command for born-attached Orchestrator windows.

The forward-looking Live/Dormant restore model (a dormant session runs a local placeholder authority "presented as its real backend, disconnected", reconnecting only on activation) is partly landed (terminal_command, per-session trust, per-session env shipped; reconnect-on-activate and warm-background-survives-restart still gated on live multi-session).


5. Workspace Trust — gating execution

5.1 The model (implemented)

A freshly opened project may contain attacker-controlled content that only matters when executed (./.venv/bin/python, a .envrc, build scripts, proc-macros). Trust is the single per-project gate. Three levels (TrustLevel):

  • Restricted (the safe default): no repo-controlled code runs. A spawn whose explicit executable path resolves inside the workspace is refused; an ordinary bare-name spawn resolved via $PATH proceeds (git/rg work). Safe because no env manager activates under Restricted, so the repo's bin/ is never on $PATH.
  • Trusted: every spawn allowed.
  • Blocked: every spawn fails.

The containment check (decide_restricted) lexically normalizes the candidate path (no filesystem touch, so it never blocks or fails on nonexistent paths) and tests it against both the given root and its canonical form (they differ across symlinks, e.g. /tmp/private/tmp). A poisoned lock fails open (Restricted's job is to stop repo execution; a poisoned lock is an internal bug, not a hostile project).

5.2 The single choke-point

Every editor primitive that runs a child routes through the active authority's ProcessSpawner / LongRunningSpawner, and every spawner calls gate(&trust, command, cwd) at the top of each spawn method — local, SSH, docker, kube alike. That is the one place covering all callers with no per-caller cooperation. spawnHostProcess bypasses the authority spawner (it must run on the host), so it consults WorkspaceTrust::decide directly at its call site.

Trust is a mandatory Arc<WorkspaceTrust> on every spawner — no Option, no post-hoc decorator wrapping; a spawner literally cannot be built without it. It is the same Arc the server owns, so the command palette / status-bar pill mutate the level through it and every spawner sees it live with no rebuild (set_level takes effect on the next spawn).

5.3 Persistence and per-session trust

TrustStore is one small trust.json per project, inside the user's data dir at <data_dir>/workspaces/<encoded-path>/never inside the repo (a repo must not vouch for itself), one file per project so concurrent fresh processes never contend. Writes are atomic (pid-tagged temp + rename). A corrupt file reads as "undecided" rather than crashing. is_decided() (recorded?) is distinct from level() (always concrete; undecided ⇒ Restricted) and drives whether to prompt.

WorkspaceTrust::for_session mints a per-session handle adopting the project's recorded level — so trusting one project never raises the live level another open session is gated against (the shared "remember this folder" registry is the per-project store itself). For an undecided folder with no executable-content markers it starts Trusted (a plain text/docs folder shouldn't block its own tooling); folders with markers stay Restricted until the user decides.

5.4 The open-time prompt (implemented; supersedes a stale doc banner)

Editor::maybe_prompt_workspace_trust is the single open-time trust prompt for every marker kind (env-shell, project manifests, devcontainer, .NET). It fires only for undecided projects, and only when executable_content_markers finds markers; an unmarked folder is silently trusted and persisted (no restricted chip, no question with no downside). The prompt is rendered by a bespoke security modal (radio group + descriptions, scrollable, "Quit" as the mandatory secondary at startup vs "Cancel" from the palette; no undecided outcome).

Note: the sandbox design doc carries a "WIP: prompt disabled, defaults to Trusted" banner. That banner is stale — the code above shows the Phase-1 prompt is active and marker-gated. executable_content_markers lists env markers (from the single default_env_detectors list so trust and env activation can't disagree), project manifests whose LSP runs project code at load, devcontainer config, and .sln/.csproj/.fsproj. Detection is passive (a shallow scan; it never runs anything).

5.5 What trust does not yet do

The "prompt each time" sub-mode of Blocked (ask per spawn rather than fail) is unimplemented — Blocked currently always fails. It needs an async UI round-trip from the spawn site. The broader queue-and-drain elaboration (gate stays sync Allow/Deny; a broadcast::Sender<TrustLevel> re-triggers denied LSP/plugin/env work on trust change, rather than blocking the spawn site) is planned (Phase 2). The Sandboxed level (replace Restricted's syntactic gate with an ad-hoc container mounting only the project) is a design proposal: the enforcement core and per-project persistence exist; the containment model does not.


6. The live environment provider

The active environment is a recipe ({snippet, dir}), not a stored snapshot — re-evaluated on demand by running the snippet on the active backend's host and capturing the result, so it can never go stale. Shared and interior-mutable exactly like WorkspaceTrust: every spawner holds the same Arc<EnvProvider>; the env-manager plugin sets it via editor.setEnv/clearEnv; no authority rebuild.

Backend-agnostic by design: EnvProvider::current(run) builds the capture script (cd <dir>; <snippet>; command env) and hands it to a caller-supplied run closure that executes it on the right host — local $SHELL -lc, SSH agent exec, the per-server ssh, docker/kube exec. That closure must be a raw spawn that does not itself apply this provider's env, or capture would recurse. A content-hash cache over the env-input files (.envrc, mise.toml, …) keeps the common path free; correctness never depends on the cache.

For the integrated terminal (a synchronous, non-tokio portable-pty path) there is a blocking delta capture (current_local_delta_blocking): dump env baseline, print a sentinel, run the recipe, dump again, diff. The EnvDelta carries only what the recipe changed over a clean login shell — volatile bookkeeping (PWD, SHLVL) diffs out — so applying it to a child's env is shell-agnostic. SSH's terminal uses the same idea remotely via a base64-encoded python3 launcher (ssh_remote_env_launcher) so the env applies as data and survives any login shell's quoting (fish-safe), reusing the identical delta sentinel. This uniform spawn(argv, cwd, env_delta, …) direction is core shipped; Local & SSH e2e-validated, Docker & Kube unit-tested but not e2e-validated.

EnvStore persists the activated recipe (env.json, alongside trust.json in the project state dir, never in the repo). for_session(dir, trusted) restores a persisted recipe only when trusted — the env gate mirrors the spawn gate — so a trusted session boots already in its env with no auto-activation restart flicker; an untrusted session restores nothing.


7. Devcontainer & Kubernetes — implemented vs planned

7.1 Devcontainer (shipped as a plugin)

The devcontainer plugin owns the backend lifecycle; core owns the slot. Flow: boot local → plugin finds .devcontainer/devcontainer.json → one-shot "Attach?" (decision stored in plugin global state keyed by getCwd(), no re-prompt) → spawnHostProcess("devcontainer", ["up", "--workspace-folder", cwd]) → parse the JSON result → build a docker-exec AuthorityPayload (filesystem local, since the workspace is bind-mounted so host/container paths coincide) → setAuthority → core restarts into the container authority. Detach is clearAuthority; rebuild is up --remove-existing-container. The userEnvProbe capture rides in SpawnerSpec::DockerExec.env.

The shipped-code gap analysis confirms the architectural divergences are intentional: not a remote extension host (the UI stays on the host; only spawned processes cross into the container, one-shot docker exec), and paths are not translated for the filesystem (bind-mount means they coincide; remoteWorkspaceFolder is passed as -w). Missing/planned (UX around the build lifecycle): image-pull/build/start state machine, live devcontainer up log streaming, cancel-in-flight, port-forwarding detection, auto-install of customizations.*.extensions; one flagged spec violation — initializeCommand is never invoked.

Fake-devcontainer-CLI test harness (shipped): pure-shell fake-devcontainer + fake-docker scripts prepended to $PATH so the plugin's devcontainer --version probe resolves the fake. devcontainer up streams fake build progress to stderr and emits the real {outcome,containerId,remoteUser,remoteWorkspaceFolder} JSON line on stdout; docker exec parses -i/-t/-u/-w then runs the command on the host (no real isolation). Failure-injection env knobs (FAKE_DC_UP_FAIL, FAKE_DC_UP_HANG, FAKE_DC_UP_BAD_JSON, FAKE_DC_UP_NO_CONTAINER_ID, …) exercise the editor's attach/lifecycle/log/cancel/parse-failure paths with no Docker daemon. Explicit boundary: it does not test in-container PATH/LSP/file-access (since docker exec runs on the host) — "all green under the fake" is not "ships."

7.2 Kubernetes (transport + plugin shipped; storage & warm sessions planned)

Shipped: the kubectl exec transport, KubeConnection, connect_kube_authority + KubeKeepalive, KubectlLongRunningSpawner, the heartbeat + reconnect tasks, attachRemoteAgent wired end-to-end, the K8s-workspace plugin's provider model, and a fake-kubectl e2e harness. v1 deliberately depends on the kubectl binary (vs kube-rs WebSocket exec) — it buys TTY-resize and the SPDY→WebSocket transition for free. The plugin owns pod provisioning via a Provider contract (attach-existing / manifest / run / command) and three preflight checks (python3 present, create on pods/exec, not on Fargate); core does only the agent bootstrap + attach.

Planned / design-doc only:

  • Storage: live tier EBS GP3, durable tier S3 reached by syncing (preStop hook + periodic), restored via initContainer. S3 is never a live mount: Mountpoint-for-S3 forbids non-replacing rename, so Fresh's temp-write-then-rename save would fail on every save; EFS is much slower on small files. Fresh imports no AWS crate — zero core change. No shipping code exists for this; it is purely design.
  • Reconnect after pod reschedule (the pod-name re-resolution callback) — load-bearing for Spot interruptions, not yet wired.
  • Warm background cloud sessions: a Cloud Workspace is an Orchestrator Session whose Authority is the kube remote-agent authority, surfaced via a generic optional "remote facet" on the session row (state glyph, rough $/hr, idle timer) — no separate panel, zero cost for local-only users. Keeping background sessions warm (heartbeat for every warm session, instant switch-back, a configurable warm-set cap) is what forces the per-session-authority refactor (§1.5) — the main thing still gated.

8. Quick implemented-vs-planned table

CapabilityStatus
Authority single slot, opaque-to-core, non-Clone, per-Window fieldImplemented
Destructive restart transition (standalone + daemon)Implemented
CommandWrap / terminal_command (local/docker/ssh/kube)Implemented
SSH authority: agent, RemoteFileSystem, spawners, terminalImplemented
kubectl-exec authority (transport, connection, spawner, keepalive)Implemented
docker-exec authority (plugin-built, userEnvProbe env)Implemented
Heartbeat + reconnect task (SSH + kube)Implemented
Respawn embedded terminals on silent reconnectImplemented
SessionAuthoritySpec persistence + reconnect rebuildImplemented
Path translation for container LSP URIsImplemented
Workspace Trust (3 levels, choke-point gate, per-project store)Implemented
Per-session trust + env (SessionScope, for_session)Implemented
Open-time trust prompt (marker-gated, single prompt)Implemented
Live env provider + delta capture (local/SSH)Implemented
set_session_authority no-restart single-window swapPartial (gated on live multi-session)
Live multi-session / warm background sessions / per-window keepalivePlanned
Reconnect after pod reschedule (pod-name re-resolution)Planned
K8s EBS-live + S3-sync storagePlanned (design only)
Sandboxed trust level (ad-hoc container)Planned (design only)
Trust queue-and-drain (broadcast re-trigger)Planned (Phase 2)
Blocked "prompt each time" audit sub-modeNot implemented
Devcontainer build-lifecycle UX (logs/cancel/state machine/ports)Planned

9. Superseded / consolidated docs

This doc consolidates the as-built picture from the following design notes. Several are aspirational or partly stale; flagged accordingly.

  • The Authority abstraction design — as-built for the abstraction itself; its "Evolution: per-session authority" section is design direction, not fully shipped (this doc records what landed).
  • The per-session-backends design — "design target, partially landed"; terminal_command / per-session trust / per-session env shipped, reconnect-on-activate + warm background planned.
  • The K8s authority design — transport/bootstrap/heartbeat/reconnect-task shipped; storage (EBS+S3) and warm-session story design only.
  • The K8s-workspace UX and plugin designs — forward-looking product/plugin framing (provider model slice shipped; warm sessions / cost facet planned).
  • The devcontainer plugin design — predates the implementation; the plugin has since shipped (see the gap-analysis).
  • The devcontainer spec gap-analysis / remediation plan — audit of shipped code + a remediation tracker (most fixes landed; lifecycle UX still planned).
  • The fake-devcontainer-CLI note — the test harness, shipped.
  • The SSH remote-editing design — pre-implementation blueprint; the SSH stack it describes has since shipped.
  • The remote-env-manager design — Done/Remaining split; trust + SSH-LSP + EnvProvider + setEnv shipped (local & SSH); its three-level "Restricted" middle is partly superseded by the trust-sandbox design.
  • The remote-filesystem-optimization note — proposal; the delta-patch / server-side-walk / streaming pieces it weighs are implemented in RemoteFileSystem, though the doc itself reads aspirational.
  • The async-remote-file-explorer design — the concrete async/timeout fix design; the bounded channel timeout + non-blocking explorer are implemented.
  • The uniform-env-activation design — "design + in-progress" but core shipped & validated for Local/SSH; Docker/Kube unit-tested, not e2e-validated.
  • The workspace-trust-sandbox design — design proposal for the Sandboxed level; its "prompt disabled / defaults to Trusted" status banner is stale (the marker-gated open-time prompt is active).
  • The trust/env/devcontainer UX plan — Phase-1 plugin/prompt UX done; queue-and-drain, per-marker hashing, clickable chips, action-log panel Phase 2 / planned.