Skip to content

[Feature]: Allow TCP listener mode #1653

Open

Description

@DarkGhostHunter

Before submitting

  • I have searched existing feature requests and discussions to confirm this has not been requested

Problem or motivation

Currently DevContainers only are local. There is no way to directly expose a DevContainer via SSH remotely, unless an SSH connection is done directly to the host to control the provider or manually do this.

I made an article ages ago about how I tackled the problem using DevPod. Essentially, opening a port with a built-in SSH server.

Proposed solution

Configuration to alter the agent to expose local + TCP for connecting. Plus, copy-pasteable configuration (script) to add to the SSH configuration of the client machine that wants to connect.

Alternatives considered

  • Running the SSH Server and using supervisord/s6 to orchestrate.
  • Connecting to the provider via SSH and exposing it.
  • Walking three steps to the other computer and work from there.
  • Walking three steps to the other computer, connect the keyboard and mouse and monitor, go back, work from there.
  • Nagging the boss about a local server to run Cursor for the sixth time.

Area

SSH / tunneling, Dev containers

Acceptance criteria

  • User should be able to checkbox "Expose remotely".
    • User should be able to manage non-su that can connect to, with/without password. (Useful to disconnect rogue AI agents).
  • User should be able to select a port that does not collide with the default 22 and 2222 for remote connection.
  • The port expose should be a proxy. One port → multiple containers, instead of one port per container.

Additional context

No response

Activity

  1. skevetter commented on Oct 31, 2026

    @skevetter
    Contributor

    Implementation plan: one remote SSH listener for multiple workspaces

    Reviewed 30 October 2026. Status: outstanding feature. The current repository has internal TCP listeners and local per-workspace tunnels, but no supported shared remote-access gateway with per-workspace authorization, connection management and Desktop controls. No open implementation PR for that complete scope was found. Keep #1653 as the feature tracker.

    Source baseline: 9bc12ee041ca854c6f81de95fd8c6e49d7cc028c. Implementation below is proposed; this investigation inspected source and current GitHub work, without running a remote-access prototype.

    1. Existing mechanisms and the missing boundary

    Existing behavior Source Design implication
    The internal SSH command already accepts an address or --stdio; its default address is 0.0.0.0:8022. cmd/internal/ssh_server.go Opening that listener alone does not implement the requested shared access product.
    NewServer installs key authentication only when keys are supplied; session handlers run commands on the machine where that server executes. Its normal forwarding callbacks allow destinations without per-workspace policy. pkg/ssh/server/ssh.go Do not expose this trusted internal server on the user's host as the remote gateway. It would have the wrong execution and authorization boundary.
    NewLocalTunnel binds loopback ports from 10800 and connects one supplied dialer. pkg/tunnel/local_listener.go Keep this IDE compatibility feature. It is neither a shared listener nor a grant system.
    WorkspaceDialer starts workspace ssh --stdio, using the selected context, user and workspace. pkg/tunnel/dial.go Reuse existing workspace transport boundaries, with an explicit gateway policy, rather than changing Docker port mappings.
    Ordinary workspace SSH performs provider readiness checks and initializes credential services. Its StartWait(..., false) rejects a stopped/missing provider machine; later container preparation has a separate startup path. cmd/workspace/ssh.go:jumpContainer, workspace_client.go:StartWait, startTunnelServices Remote guests need an explicit start-existing policy. Authenticate before provider work and never forward the owner's credential helpers implicitly.
    Existing transports expose an intermediate container SSH client. ContainerTunnel.AsUser selects its SSH username; it does not prove the server process dropped privileges. The SFTP handler runs in that server process and only changes its working directory. pkg/tunnel/container.go:runInContainer, sftp_handler.go Do not bridge guest channels to the intermediate client. A non-root shell username alone would leave SFTP and forwarding in the intermediate server's process identity.
    Ordinary SSH launches a final internal ssh-server --stdio --track-activity, using su when needed; its activity writer protects the connection and releases protection on exit. cmd/workspace/ssh.go:buildSSHServerCommand, cmd/internal/ssh_server.go:serveStdio Extract this final-server launch boundary without owner credential services, verify its actual UID/GID, and bridge only to that per-user server.
    The local Desktop listing daemon has a five-minute idle timeout and a Unix socket explicitly chmodded to 0666. pkg/daemon/local/daemon.go, socket_unix.go Do not add gateway grant/revoke mutations to that socket or make listener lifetime depend on its listing cache.
    Supervised background workers already have process ownership and launch leases. pkg/command/background.go:StartSupervisedBackground Reuse those lifecycle primitives for a dedicated opt-in worker instead of implementing PID-only process control.

    The article in the request explains the original one-container workaround. This design addresses the request's additional shared-port, user-management and revocation requirements.

    2. Product scope and explicit refinements

    Provide one host-side SSH gateway per OS user and Devsy home, with one configured listener and multiple authorized workspace grants. Clients use ordinary SSH, SFTP and a compatible remote SSH IDE. They do not need Devsy or provider credentials installed locally.

    The first version supports locally managed workspaces accessed through existing Devsy clients. Platform-managed workspaces remain controlled by their existing platform authorization and are rejected from this local sharing surface. Provider access remains owned by the host user; guests receive only a grant to an existing workspace generation.

    The issue's user-management requirement becomes named public-key grants to an existing non-root container user. A grant binds a client key to one workspace and one run-as user. The feature does not create Linux accounts or change sudo policy. Reject UID 0 and a missing user when enabling access. A non-root account with sudo, a mounted Docker socket or access to shared secrets retains those existing privileges; this gateway is a connection access control, not a new tenant sandbox.

    Password authentication is deferred. Public-key authentication permits password-free client use while keeping individually revocable identities. Do not include a no-auth mode or interpret an empty authorized-key list as unrestricted access. If password management is later required, track it as a separate extension with its own storage and authentication design.

    Remote connections may prepare an existing authorized workspace. This includes explicitly starting its existing stopped provider machine, where supported, and starting or restoring its runtime container through the existing runner.Up(NoBuild:true) path, including that workspace's configured startup lifecycle. cmd/internal/container_tunnel.go:startDevContainer/startContainer already performs that container preparation; starting a stopped provider machine is an explicit new gateway policy. State these effects in the access settings and generated configuration output. Authentication, grant validation, local workspace-UID lookup and revocation checks occur before any provider/start operation; live container-user verification follows startup. A connection cannot create a new logical workspace record or provider machine, request an image build or rebuild, choose a provider, select an arbitrary path or change its run-as user. Do not call workspace resolution paths that create a record when the stored UID is missing.

    Closing Desktop's window does not stop the worker. It runs until remote access is disabled or gateway stop is issued. No OS login service, firewall rule, router mapping or hosted relay is installed. Persisted enabled settings may restart the worker when Desktop next starts; an OS restart alone does not install a new automatic service.

    3. Routing and authentication contract

    SSH does not supply a TLS-style server-name routing field for a raw TCP proxy. Authenticate at the gateway and select the workspace from a server-owned grant identifier in the SSH username. Do not inspect an unauthenticated shell command to choose the target.

    Generate an opaque identifier such as g-<32 lowercase hex characters> for each grant. It is a routing identifier, not a secret. The presented public key must match that grant, and the grant must be enabled. The grant's stored context and UID identify the target; mutable display names, local SSH aliases and client-provided paths do not authorize access.

    Example generated client configuration:

    Host horse-project
      HostName devbox.example.net
      Port 22022
      User g-0123456789abcdef0123456789abcdef
      IdentityFile ~/.ssh/devsy-workstation
      IdentitiesOnly yes
      HostKeyAlias devsy-gateway-GATEWAY_ID
      UserKnownHostsFile ~/.ssh/devsy-gateway-known-hosts
      StrictHostKeyChecking yes
      ForwardAgent no

    The example username is illustrative; commands must output the actual generated grant. Also output the single public host-key line required for the named known-hosts file and its SHA256 fingerprint. Never output the host private key or the owner's workspace private keys. The client supplies its own identity-file path; host-local paths must not appear in the snippet.

    Persist one Ed25519 gateway host key before the first listen and reuse it across worker restarts. Replacing it is an explicit rotation operation, never an incidental rebuild or workspace change. Client config must not inherit the internal generated SSH configuration's StrictHostKeyChecking no behavior.

    A public key may have separate grants to multiple workspaces, all on the same port. At handshake completion, pin the selected grant and workspace-access generations, context, workspace UID and run-as user to the connection. Recheck both generations before starting the backend and before accepting every new channel. Revocation closes all matching connections and in-flight backend startup, not just future handshakes.

    4. Stored state and lifecycle

    Add a focused pkg/gateway package with separate policy, SSH transport, connection bridge and worker-control files. Use the existing configuration path manager to derive a private directory under the selected Devsy home. Do not place mutable control state at a global /tmp/devsy-local.sock path.

    Versioned policy file, written atomically under a cross-process lock:

    Record Required fields
    Gateway schemaVersion: 1, generated gatewayId, enabled, listenAddress, advertisedHost, generation.
    Workspace access context, workspace UID, enabled flag and generation. This is the workspace's access switch, independent of each grant's revocation state.
    Grant generated id, human label, context, workspace UID, last-known workspace ID for display, exact container runAsUser, verified effective UID/GID, authorized public key, key fingerprint, enabled flag, generation and created timestamp.
    Worker state schema version, gateway ID, process reference/launch identity, policy generation actually applied, listening address, start time and status. No private keys.
    Live connection generated ID, grant ID/generation, workspace-access generation, workspace UID, authenticated key fingerprint, remote address, started time and phase (connecting or connected). In-memory, bounded.

    Policy/key directory is owner-only; policy and private key files use 0600-equivalent permissions. Windows uses an owner-restricted ACL. Reject unsupported schema versions, malformed keys, oversized files and symlink redirection outside the owned directory. Cap initial policy at 128 grants and 1 MiB. Preserve existing state when validation fails.

    Use an owner-only control transport: Unix socket in the private directory with mode 0600, and a Windows named pipe with an explicit current-user ACL and a path scoped to user plus canonical Devsy home. Reuse the platform socket implementation patterns and go-winio dependency, but do not copy the local listing daemon's world-accessible permissions. Control requests are bounded JSON and include gateway ID and policy generation. They are never served on the remote SSH port.

    Mutations while the worker is running go through that control transport. The worker serializes policy changes, atomically persists them, applies them, closes revoked connections and acknowledges the applied generation. Offline edits acquire the same policy/start lock and require verified absence of a live worker or pending launch lease. A control timeout or permission error is not evidence that the worker is offline: report failure instead of silently editing policy behind a running worker. On restart, load the latest policy before listening. Do not acknowledge a revocation based only on writing a file that a worker might not reread.

    Use StartSupervisedBackground, its launch identity and existing process-control primitives for start/stop. Scope the worker name to the gateway/home identity, retain its launch lease in the child, and use creation-time process ownership on Windows. start explicitly enables the gateway and waits for a bounded ready response from the actual worker; a nil return from StartBackgroundOnce is not proof of readiness. stop persists global enabled=false, disables acceptance, cancels backend startup and closes active connections, with bounded cleanup; reopening Desktop must not undo that explicit stop. It preserves grants and per-workspace choices for a later explicit start. A stale PID must never terminate an unrelated process. Repeated start/stop is idempotent, and separate Devsy homes do not share a listener worker.

    Default listener is 127.0.0.1:22022 until the owner chooses a remote-reachable bind address. Expose remotely must show the configured address and require a non-loopback selection to claim remote availability. Accept an explicit IP address and a port from 1024-65535 excluding 2222; do not scan to a different port on collision. advertisedHost is a hostname/IP chosen for client configuration, not 0.0.0.0 or ::. Pre-bind a replacement endpoint before committing it when it does not overlap the current socket. For overlapping binds on the same port, such as loopback to wildcard, use a serialized close/bind operation with attempted rollback; pre-binding cannot succeed while the old overlapping listener remains open. Persist the new endpoint only after its bind succeeds. On failure retain the old policy and restore its listener when possible; if restoring also fails, report the actual stopped/bind-failed state and both errors. An endpoint change may interrupt acceptance and disconnect sessions, and the settings action must say so.

    5. Implementation tasks

    A. Backend access boundary

    • Add pkg/gateway/policy.go, store.go and tests for the records/validation above. Add owner-scoped paths through pkg/config/pathmanager.go. Use existing atomic-write conventions and cross-process locking; do not build a separate database.
    • Implement exact UID lookup across persisted workspace records in the stored grant's explicit context, before remote provider discovery. Resolve the current ID only after finding that UID, load it with the existing local-only retrieval boundary, and recheck the UID. Reject a missing/different UID, ambiguous/corrupt records or platform-managed workspace. Update the cached display ID after a legitimate ID rename without changing authorization. Rebuilds that retain the workspace UID still require the runtime identity validation below.
    • Grant creation validates that the named container user exists, resolves to a nonzero UID, and has an effective UID/GID compatible with the runtime's user policy. Resolve the default from the current devcontainer result only when it is a valid non-root user; require an explicit existing user otherwise. Persist the verified identity with the grant. Use structured exec arguments or the existing shell-quoting boundary. Revalidate after every backend startup; a changed account/UID/GID fails closed and requires the owner to update the grant, rather than silently authorizing a replacement identity.
    • Build a backend connector that owns the entire transport chain for one authenticated remote connection. Extract only the needed dispatch from cmd/workspace/ssh.go, preserving proxy clients' OpenSSHTransport, machine clients' ContainerTunnel, and managed connection cancellation. Its public result is the *ssh.Client connected to the final per-user server, never the intermediate transport client. Use a small options struct containing expected workspace UID, fixed user/UID/GID and gateway policy; provide one idempotent close/cancel function for all layers.
    • Extract the command-building boundary from buildSSHServerCommand: over the intermediate client, open a managed session executing the existing internal ssh-server --stdio --track-activity --workdir ... path under the grant's real account. Preserve the existing correctly quoted su transition when the intermediate process is privileged; do not combine ContainerTunnel.AsUser(user) with a second unconditional su. If the intermediate process already runs as the selected user, do not attempt another privilege transition. Fixed non-root runtimes must reject a requested identity different from their actual runtime UID/GID. No fallback to root or the default owner is permitted.
    • Add an internal expected-UID/GID startup guard to cmd/internal/ssh_server.go, using actual OS process identity before serving, and pass its values through that extracted launch path. This closes the gap between a successful shell identity probe and the server that handles SFTP/forwarding. Establish a second managed SSH client over that stdio session using the fixed selected user, and expose only this final client to the channel bridge. Shell, SFTP and direct forwarding consequently run in the same verified non-root server process identity; an incoming grant username is never reused as the container username. An older agent lacking this guard produces an explicit upgrade/unsupported result, never a retry without the guard.
    • For machine clients, recheck the stored UID while holding the normal workspace operation lock. Apply a start-existing-only policy: StatusStopped may call Start, then wait for readiness; StatusNotFound is rejected. Do not pass true to StartWait, because that also allows Create. Continue with the established container preparation path and release the lock once the final user-bound connection is ready. Retain connection-scoped cancellation after releasing it. For proxy clients, use their existing transport contract and enforce the same final-server identity guard; do not substitute generic Up for an unsupported start-existing operation. A provider that lacks the required capability returns an explicit unsupported error before a share is advertised as usable.
    • Do not start startTunnelServices, GitHub/Git credential forwarding, Docker credential injection, Git signing, GPG relay or host SSH-agent forwarding for guest connections. The gateway's backend connector must not inherit cmd.User or forwarding flags from unrelated host process state. Guest sessions still receive the workspace's own configured environment and permissions.
    • Retain --track-activity on that final server and the protected context from cmd/internal/ssh_server.go:serveStdio / pkg/daemon/agent/activity.go:StartActivityWriter. Activity registration failure in a managed runtime fails the backend connection; do not substitute a best-effort host-side heartbeat. Close the final server and its activity lease when revoked/disconnected. A pre-auth probe or gateway idle listener never opens a backend. Denied channels must not create an additional backend/activity lease; an already authenticated connection retains its one existing lease until disconnection.

    B. SSH listener and channel bridge

    • Add pkg/gateway/server.go using the already-used golang.org/x/crypto/ssh connection APIs and explicit public-key callback. Every inbound handshake requires an enabled gateway, enabled workspace-access record and valid enabled grant. The callback only checks the proposed key/policy; do not start a backend from it, because a client may query a public key before proving possession. Start provider work only after ssh.NewServerConn succeeds and a fresh generation check passes. Unknown grant, wrong key, any disabled gate and revoked generation fail before provider I/O. Do not call pkg/ssh/server.NewServer as the host-side public listener.
    • Add pkg/gateway/bridge.go. Open backend SSH channels only through the final per-user client described above. Copy normal and extended-data streams separately, preserve EOF/half-close, requests, exit status/signals and cancellation. Do not parse a guest command and run it through a host shell, local exec.Command, Docker CLI argument or provider command. Reject unsolicited backend channel opens; forward only the corresponding allowed request/notification types for an authorized channel.
    • Support session channels with bounded pty-req, terminal modes, shell, exec, subsystem=sftp, window-change, signal and the corresponding backend exit notifications. Preserve request success/failure and want-reply. Permit only one shell/exec/subsystem start per session. Handle binary SFTP data without line parsing or logging it.
    • Permit env requests only for TERM, LANG and LC_*, with bounded names/values; do not allow a guest to inject host-side environment or override gateway policy. Forward approved requests to the backend session.
    • Support IDE direct-tcpip channels only to literal container loopback targets (127.0.0.1, ::1, or normalized localhost) and valid ports. Execute the dial through the container SSH client's forwarding channel, never through host net.Dial. Preserve origin fields as metadata only. Reject arbitrary remote destinations.
    • Reject gateway-side host shell access, remote TCP listeners (tcpip-forward), Unix socket forwarding, X11 forwarding, agent forwarding and unknown channel/request types in this first version. Return protocol rejection rather than silently accepting an unimplemented request. An IDE needing an excluded capability must show a documented unsupported result, not trigger permissive fallback.
    • Add bounded admission: at most 16 unauthenticated handshakes, 64 authenticated connections and 32 channels per connection; 10-second pre-auth deadline and existing bounded backend bootstrap deadlines. Use bounded copy buffers and backpressure, not accumulating stream output. Handle keepalive@openssh.com locally on the public transport and allow its reply; it is not a routing request. Extract/reuse the existing server keepalive configuration from pkg/ssh/server/ssh.go (15 seconds and 8 failures by default), with bounded sends and cancellation, because x/crypto/ssh does not inherit the current wrapper's timer. Keep inner managed transport health checks. Do not use a short terminal-idle timeout as a substitute for liveness.
    • Record connection metadata, authentication failure class, workspace UID, close reason and duration with the existing logger/redaction conventions. Never log private keys, complete command lines, stdin, terminal output or file contents. Bound/rotate worker logs using an existing project log mechanism.

    The channel behavior follows the SSH connection protocol, particularly session requests, extended data, channel closure and port-forwarding channels. This bridge must preserve those semantics rather than treating every connection as one undifferentiated byte stream.

    C. Owner controls and CLI

    Add public cmd/gateway/ commands and register the parent in cmd/root.go:

    # Set the single endpoint used by every grant.
    devsy gateway configure --listen 192.168.1.20:22022 --advertise-host devbox.example.net
    
    # Authorize one client key for one existing workspace/user.
    devsy gateway grant project-42 --context default   --name workstation --public-key-file ./workstation.pub --user developer
    
    # Enable this workspace, start the host worker, then print client config.
    devsy gateway enable project-42 --context default
    devsy gateway start
    devsy gateway config GRANT_ID
    
    devsy gateway sessions
    devsy gateway disconnect CONNECTION_ID
    devsy gateway revoke GRANT_ID
    devsy gateway stop
    • Implement configure, grant, list, config, enable, disable, start, serve, status, sessions, disconnect, revoke, and stop as thin wrappers around the same typed policy/control API. enable/disable take an existing workspace and explicit context; they change the workspace-access gate. New workspace-access records start disabled, and adding a grant does not implicitly enable access. serve is the foreground worker and suitable for an owner-managed service. config returns the SSH snippet, public known-host line and fingerprint, without writing another computer's files.
    • Follow existing --context, --home, completion, CLI error and --result-format=auto|plain|json conventions. Add canonical names to pkg/flags/names/names.go where needed. Public key files are read locally with a size limit; reject private-key input and unsupported authorized_keys options rather than ignoring restrictions supplied by the user.
    • Add workspace enable/disable operations to the typed API using the explicit workspace-access record. Disabling the workspace rejects all its grants and disconnects active sessions. Re-enabling it restores only still-valid grants, never a revoked key. A grant revocation disables that specific grant permanently; authorizing that key again creates a new grant ID/generation. It affects every connection opened under the revoked grant, including multiple IDE connections.
    • Hook successful workspace/context deletion into policy revocation using UID, while the gateway independently fails closed on every missing-UID lookup. Reconcile active grants on a bounded interval so deletion outside Desktop cannot leave sessions indefinitely authorized. Rename keeps grants by UID. An imported/copied workspace with a new UID never inherits access automatically.
    • Disconnection revokes the transport, not arbitrary detached processes already started inside a container. Document this distinction in connection controls and acceptance; job/process supervision remains the owner's separate responsibility.

    D. Desktop integration

    • Add shared request/response types in new desktop/src/shared/gateway.ts, plus a focused CLI adapter in desktop/src/main/gateway.ts. Keep the CLI worker authoritative; renderer state is a view of current worker/policy state.
    • Register typed gateway IPC in desktop/src/main/ipc.ts and inject lifecycle dependencies from index.ts. Apply the existing main-window/trusted-sender checks used by sensitive IPC before key/grant/listener mutations. Renderer inputs contain workspace ID/context/expected UID and public-key text or a main-process file-picker result, not arbitrary control socket paths.
    • Add a Remote access section to SettingsPage.svelte for the single listener, advertised hostname, running state and fingerprint. Show actual worker state, including disabled, starting, listening, bind failed and stopped. Do not display enabled solely because the configuration says true.
    • Add a compact Expose remotely section to the existing workspace detail view. Show the fixed non-root container user, named key grants, key fingerprints, copyable SSH configuration and active connections. Start disabled. Turning it on requires a configured listener and a valid grant; turning it off revokes that workspace's active access.
    • Provide Disconnect per connection and Revoke key access per grant. Save explicit settings, keep pending form values on an error, and use the existing compact toast/error-detail patterns. Never send a private key through IPC or store one in renderer state.
    • Use bounded polling while the view is open, one in-flight request, explicit context and stale-response generation guards. Closing the view cancels its reader only. It must not stop a worker or another workspace operation.
    • Add wrappers/mock fixtures under desktop/src/renderer/src/lib/ipc/, component tests and Playwright flows. A display-label/SSH-alias update from [Feature]: Separate workspace label from host name (alias) #1654 only changes presentation; grants remain bound to context and UID.

    6. Verification and acceptance

    Deterministic tests

    • Empty key lists, disabled gateway/workspace access, wrong key, unknown grant and revoked generations reject with zero backend/provider calls. An unsigned public-key query for a known valid grant also causes zero backend calls. A valid grant cannot select another UID, context or run-as user by changing username or request payload.
    • Identity lookup survives ID rename and rejects delete/name reuse, import with new UID, context deletion, nonexistent user and UID 0. Two grants with the same key still enforce different workspace/user bindings. The final server startup guard rejects an actual UID/GID mismatch, including fixed non-root runtime mismatches and changed account identities; no channel is exposed before verification.
    • Channel bridge carries binary stdin/stdout/stderr, PTY input/resize, EOF, signals, exit codes and SFTP correctly. Backend failure closes only that connection. Unknown channels and denied forwarding have explicit rejection behavior.
    • Direct TCP requests reach only the selected container's loopback; host DNS, host loopback and arbitrary remote destinations are never dialed. Reject malformed or oversized request payloads before allocation/dial.
    • Assert that owner SSH/GPG/GitHub/registry credential service hooks are not invoked by gateway sessions. No command output or input appears in logs.
    • Revoke during authentication completion, backend startup and channel-open races; verify no later channel is accepted and all matching sessions close. Revoking one grant leaves other grants and local owner SSH sessions connected. Disable/re-enable a workspace and stop/start the worker without resurrecting revoked grant IDs. An unreachable live control endpoint reports mutation failure and never falls back to an offline policy write.
    • Worker restart preserves host key and latest revocations. Concurrent starts produce one listener. Bind conflict is an error, not port scanning. Test both non-overlapping replacement and loopback-to-wildcard same-port changes, including a failed replacement and failed rollback with accurate stopped state. Stale process state cannot kill another process. Unix control permissions and Windows pipe ACL prevent a different local user mutating grants.
    • Configuration-write failure leaves prior policy active and reports failure; successful revocation acknowledges the applied generation. Limits and shutdown tests demonstrate bounded goroutine/connection cleanup.
    • UI tests cover disabled default, actual listener status, explicit Save, grant-specific errors, non-root user requirement, key revocation, stale context results and independent reader cancellation.

    GitHub Actions integration and native acceptance

    Extend existing Ginkgo SSH/up tests and their registered CI suites; use a real second SSH client process and two real workspaces. Do not accept only an in-process mocked proxy test.

    Scenario Required observation
    Two workspace routes, one port Two authorized grant usernames connect simultaneously through the same host TCP port and read different marker files from their assigned workspaces.
    Existing stopped workspace An authorized connection starts its existing provider machine/container as supported; a missing provider machine never invokes Create. Restoring a missing runtime container stays on the configured existing workspace's NoBuild path. An invalid key/UID causes no start, build, creation or provider call.
    User boundary Verify actual final-server UID/GID and shell id against the grant. In an isolated test container without guest sudo, a root-owned 0600 sentinel is unreadable through SFTP and exec, while a guest-owned file transfers successfully. Attempting a different SSH username or exec payload cannot change the target or execute a host shell. Check fixed non-root runtime identity mismatch fails before any channel opens.
    IDE workload A supported VS Code-family client opens multiple sessions and its loopback forwarding channels; a shell and SFTP transfer work at the same time. Record any IDE capability exclusions.
    Revocation Revoking one grant disconnects its terminal and IDE transports, including pending startup; another grant and the owner's ordinary local connection continue. New attempts with the revoked grant fail.
    Runtime lifecycle Stop/restart and inactivity accounting behave correctly: verify the final per-user server registers one managed activity lease, a valid connection protects the workspace, and revocation/disconnection releases it so the normal inactivity deadline applies. Unauthenticated probes and an idle listener add no lease. Missing managed activity ownership rejects the backend rather than creating an untracked connection.
    Identity lifecycle Rename keeps the same grant; delete/recreate with the same ID denies the old grant; context deletion also removes authorization.
    Listener lifecycle Port conflict is visible, restarting the worker preserves fingerprint, and closing/reopening Desktop neither duplicates nor unexpectedly stops the worker.
    Native hosts Run socket/worker process tests on Linux, macOS and Windows. Record a second-machine/LAN connection using the generated config and strict host-key check before declaring the Desktop remote-access flow complete.

    Implement in signed commits and Draft PRs. Suggested review slices are policy/owner control, backend connector and channel bridge, then CLI/Desktop/customer acceptance. Keep all slices linked to this issue and keep it open until the actual shared-port, authenticated, revocable customer flow is delivered.

    7. Excluded work

    No per-container published SSH port, automatic router/firewall changes, hosted coordination service, password database, guest workspace creation, platform authorization bypass, automatic Linux-account creation or guarantee of process isolation within a shared workspace. Existing stdio SSH and loopback IDE tunnels keep their current contracts. The user-facing outcome is one explicitly enabled host listener with named, revocable access to selected existing workspaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions