跳到主要内容

Loop 0.5.0 · 2026-10-31

文档对应最新 npm 发布版本,保留英文原文。

Private Tunnel Connection

Loop manages the official Dev Tunnels CLI to sign in to Microsoft and host a private tunnel. A separate loopback ingress serves the built application to explicitly paired devices after the service’s own access checks. Unpaired devices can open the pairing page but cannot access business APIs. See device pairing and remote authorization.

Setup and Usage

Start Loop normally, then open Settings → Remote access:

  1. Select Install Dev Tunnels CLI in the first step. Loop downloads and verifies the official tool if needed. No terminal commands, Microsoft application registration, Client ID or scopes are required.
  2. Select Sign in in the second step and complete Microsoft sign-in in the computer's system browser. Personal Microsoft accounts, including Outlook accounts, use the official CLI's Microsoft login; organizational policies and service consent can still apply.
  3. Select Connect Tunnel in the third step. The completed third step and Disconnect Tunnel action indicate connection; the settings screen hides the HTTPS address and redundant connected caption. Step 4 automatically shows a QR invitation after connection. Its URL uses this tunnel origin plus a one-use pairing fragment.
  4. The download cancel icon cancels installation; Disconnect Tunnel stops hosting through the existing stop command. The tunnel step shows one plug button, disabled while connecting or disconnecting, without a separate cancellation icon. Sign-in has no stop icon and settles on success, failure or timeout. Closing the settings tab does not stop the Web process.

Completed steps show green checks; incomplete steps show empty circles, with actions unlocked in order. The heading's info button shows the license terms and privacy statement on hover, focus or click. The local API still supports automatic installation before login or connection. With an installed CLI, login starts in signing-in and an already authorized connection starts in connecting, including during executable verification. Only a missing or invalid executable returns these operations to installing for a verified replacement. Sign out in the sign-in step is available while connected as well as after hosting stops. It first finishes tunnel shutdown in the stopping phase without changing signedIn or its persisted marker. Only then does it enter signing-out, clear the marker and invoke CLI logout. Shutdown failure is assigned to Step 3 and preserves sign-in without invoking logout; account-stage failure is assigned to Step 2. The CLI account store is shared with other uses of Dev Tunnels under the same OS user: logout can affect those uses too. It does not sign the Microsoft browser session out or revoke consent.

After a successful connection, Loop saves a reconnect preference and rehosts the saved persistent tunnel asynchronously on the next backend launch. Startup reports connecting until CLI readiness; it never restores a connected phase or an old address from disk. Every entry into Remote access checks the installed executable and returns the persisted sign-in result and current host state. It never invokes CLI account commands or reads the credential store. Checking never downloads, signs in or starts/stops hosting; a missing or invalid executable clears the local remembered sign-in. Concurrent entry checks share one probe; during an active operation they return its current state without running competing CLI commands. Installation checks validate only the executable file. Startup restores the last successful Loop sign-in from <agentDir>/web-ui/remote-access/sign-in.json. This version-1 file contains only version and signedIn, with no account identifiers or tokens. Writes are serialized and use a synced mode-0600 temporary file followed by atomic replacement. Missing, unreadable, invalid or unsupported-version files start unconfirmed. Start reuses a remembered login without calling login or user show again; when unconfirmed, it signs in through the CLI, which may reuse its own credentials. No old host process or connected status is reused; the saved tunnel identity and port are reused after CLI readiness. Independent Web processes do not coordinate these state files; the last atomic replacement wins.

Development Restarts

pnpm dev runs a stable gateway and a separate business worker. The gateway owns Vite, the desktop listener, Tunnel lifecycle, pairing requests, device cookies and CSRF state. Backend, shared protocol and core TypeScript edits restart only the worker after a short debounce; frontend edits use HMR locally and automatically rebuild the remote production frontend. The build watcher starts with pnpm dev, serializes builds, retains the last successful build on failure, and stops with the development command. Changes to gateway or Tunnel implementation require manually restarting the development command.

During worker startup or restart, remote document requests receive an uncached HTTP 503 recovery page that retries after two seconds. Business requests receive backend_restarting with Retry-After; writes are never queued or replayed. Existing SSE connections close and the browser reconnects using the normal snapshot/replay contract. Browser reloads and disconnects cancel upstream response streams without logging expected cancellation as a server error; SSE streams also end quietly when their worker is replaced, allowing browser reconnection. Unexpected upstream failures and incomplete non-SSE responses still fail instead of appearing successful. Running model calls are interrupted by worker restart; saved history is recovered, not resumed generation. The gateway forwards business calls only to its loopback worker using an ephemeral IPC-provided capability; remote Host, Origin, device and CSRF checks run before forwarding.

Stopping the entire development command still stops the Tunnel. Production startup and direct web --dev retain their existing single-process lifecycle. This does not bypass Microsoft's sign-in or fix failures on an external authentication page.

Installation and Credentials

Loop pins CLI version 1.0.2115+e16833f284, its official HTTPS artifact URL and SHA-256 digest per supported platform. Automatic installation supports macOS and Linux x64/ARM64, and Windows x64. Other targets report an unsupported-platform error. Platform runtime and credential-store requirements are those of the official CLI.

The executable is saved below the data root at bin/devtunnel/<version>/devtunnel (devtunnel.exe on Windows), defaulting to ~/.loop/bin/devtunnel/<version>/devtunnel. Setting LOOP_DATA_DIR relocates this directory with the rest of Loop's data. Downloads use a private temporary file, a 100 MiB limit, a cumulative ten-minute active-transfer deadline that excludes paused time, digest verification and atomic rename. Every use checks the installed digest; invalid content is replaced through a verified download. Loop neither modifies PATH nor installs a system package. It does not reuse arbitrary executables from PATH or accept executable paths, commands or download URLs through the API. Updating the pinned release requires a Loop update.

Uninstall deletes only the current pinned executable from the managed directory, without invoking the CLI, logging out, reading Keychain credentials or changing system installations. Missing files count as already removed. Unexpected directories are not recursively deleted. Starting uninstall clears and persists the remembered sign-in before removing the executable; a failed removal leaves installation confirmed but sign-in unconfirmed. Successful removal resets installed and signedIn; CLI credentials remain available for later explicit sign-in. Removal errors report cli_uninstall_failed. Uninstall is rejected during a check, download, sign-in, stopping or hosting.

After the transfer completes, download.stage becomes verifying while the controller remains installing and installed stays false. Pause and Resume are unavailable during full-file digest verification, flush, permission setup and atomic placement. Only successful installation sets installed to true and clears download; verification failure or cancellation removes the temporary file without completing the step.

During downloading, Pause aborts the current request while retaining the private temporary file. Requests pin Azure Storage API version 2023-11-03 to obtain standard ETags and conditional range handling. Resume uses HTTP Range and If-Range when a validator is available; a full response truncates the partial file and restarts from zero. Invalid or inconsistent range responses fail closed. The complete file must still pass the pinned SHA-256 check before installation. Stop, graceful shutdown or failure removes the incomplete file; Stop never deletes an already installed executable. Paused downloads remain in the current Web process and are not restored after a restart.

The CLI owns browser authorization and credential storage. On macOS, explicit login, logout and hosting can request access to the system login keychain. Automatic refresh never runs user show; it retains the persisted sign-in result and clears it when the executable is absent or invalid. Reauthorization and sign-out clear the remembered result before invoking account commands. Login completion is shown only after its successful result is saved; failed state writes report sign_in_state_failed in the responsible step. Failed reauthorization or cancellation before CLI success leaves the marker false. Stop, shutdown and ordinary tunnel failures preserve it. This marker does not verify credentials: external CLI logout, credential-store edits, expiration or service revocation may make it stale. No background credential probes run; reauthorization after an actual authorization failure requires explicit Sign out and Sign in. Loop does not automate approval or modify Keychain access permissions. Failed frontend status checks stop automatic polling and require manual Retry. Account operations are never retried automatically; they report failure on CLI exit or their deadline. A Keychain Deny response is managed inside the CLI and cannot be observed directly by Loop; sign-in settles on CLI exit or its deadline. The control API still accepts Stop to cancel an operation, but the sign-in step does not expose a stop icon. Loop does not use MSAL, send Microsoft tokens to frontend code, scrape login output for tokens, or maintain a separate credential cache. Raw CLI diagnostics are not returned to the UI or written to Loop logs. Cancel terminates the child process, with forced termination after two seconds if needed. Browser windows already opened by the CLI can remain open.

Browser sign-in has a three-minute deadline; account commands have a thirty-second deadline and initial hosting readiness has a sixty-second deadline. A failed operation reports a fixed error code and can be retried. Unexpected host exit clears the address and closes the isolated ingress. Hosting status reflects CLI readiness; temporary network interruptions inside a running CLI are not independently detected by Loop.

Tunnel Lifecycle

The reconnect preference is stored at <agentDir>/web-ui/remote-access/connection.json as version 1 with only an enabled boolean. It uses the same serialized, synced mode-0600 temporary writes and atomic replacement as sign-in storage; it contains no tunnel IDs, URLs, account details or credentials. A successful Connect saves enabled=true before reporting connected. A missing, unreadable, malformed or unsupported-version record defaults to false. Existing connections from before this feature need one successful Connect to create the preference.

At startup, an enabled preference verifies the installed CLI and remembered sign-in before launching one fresh host using the CLI credential store. Restore never downloads the CLI or invokes login automatically; missing prerequisites clear the preference and leave hosting off. Host startup may still request OS credential-store approval. Failure is reported in the responsible step without a retry loop or a false connected state; the enabled preference remains for a later backend launch. A normal shutdown or unexpected host exit preserves the preference. Explicit Disconnect, sign-out, reauthorization, uninstall and detection of a missing or invalid executable clear it. Stop waits for an in-flight connection save before clearing, so a late completion cannot re-enable the preference. Disconnect still closes the host if saving fails, but reports connection_state_failed in Step 3 and does not proceed to CLI logout; retry after fixing storage permissions to clear the stale preference.

The first Connect saves a generated tunnel name and randomly assigned loopback ingress port, creates a private persistent tunnel with a thirty-day inactivity expiration, and configures its single HTTP port. It saves the canonical CLI tunnel ID before completing port setup. Every later Connect binds that same local port, verifies that the tunnel has exactly that HTTP port, and hosts the saved ID. An occupied port, missing tunnel or unexpected port configuration reports failure instead of changing the address. Port setup uses separate port commands; hosting an existing tunnel never batch-updates ports. It passes --host-header unchanged and --origin-header unchanged so relay requests retain the public Host and Origin required by the pairing ingress checks; the CLI defaults would rewrite both to localhost. Restart the Web backend and reconnect an existing tunnel to apply updated hosting arguments. It never enables anonymous access. The first verified HTTPS origin is saved and later connections must report the same origin. Access policy and transport are managed by the official CLI and service.

Stop first closes the ingress, invalidates pending pairing and terminates remote request/stream access, then shuts down the host process. Disconnect, sign-out, uninstall and ordinary shutdown preserve the persistent cloud resource and saved identity. The address remains stable across reconnection and backend restart with the same data root and authorized CLI account. The service can expire a tunnel after thirty days without activity; activity renews this sliding window. External deletion or expiry causes connection failure and does not silently create a replacement. CLI output is parsed only for the pinned version's readiness marker and port-specific HTTPS address; inspect URLs are not presented as connection addresses.

The strict version-1 file at /web-ui/remote-access/tunnel.json stores the tunnel ID, local port, setup-completion flag and optional verified HTTPS origin; it contains no access tokens or account details. Writes are serialized, synced and atomically replaced using mode-0600 temporary files. Missing state starts initial provisioning; malformed or unreadable state fails closed. Setup resumes with the saved name/ID after interruption, and never uses the CLI global last-used tunnel. One Loop process must own this data root and port. Existing temporary connections migrate to a new stable address on their next Connect; phone browsers need one new pairing on that address. The former SDK environment variables and MSAL cache are no longer read. Existing local records are left untouched; this change does not erase previously stored credentials or cloud resources.

Local Control API

Request Behavior
GET /api/remote-access Returns sanitized status and sets a process-lifetime HttpOnly, SameSite=Strict local control cookie
POST /api/remote-access Accepts exactly one action field: install, uninstall, login, start, stop, logout, pause or resume; requires the cookie and exact same-origin Origin

Example request body:

{ "action": "start" }

POST returns HTTP 202 with the accepted state. Status includes supported, installed, signedIn, phase, optional download (received and optional total bytes, optional paused boolean and optional stage (downloading or verifying; omission means downloading)), url, optional failedSteps (install, login, start), sanitized error and optional errorStep (install, login, start). errorStep identifies the stage where an operation failed, including generic CLI errors and failures during automatic prerequisites; it is cleared when another command starts or an operation is intentionally stopped. The frontend uses it to display errors within the responsible setup step. signedIn records the last successfully persisted Loop login, restored on startup; it is not an independent credential-store or service-authorization probe.

The optional failedSteps list records previously completed steps that later fail: an absent or invalid installed executable, failed reauthorization, or unexpected exit of a connected host. Successful recovery clears only the affected failure. Intentional stop clears the connection failure, sign-out clears the login failure, and successful uninstall clears all step failures. This metadata lasts only for the current process and never requires automatic credential probes. A failed refresh request returns HTTP 503; failure to clear a missing executable’s remembered login can leave its in-memory login unconfirmed, the frontend marks its cached completed steps as failed checks until a successful retry.

Phases are off, installing, uninstalling, signing-in, signing-out, connecting, connected, stopping and error. Stop can cancel an active action, including paused downloads. Pause and resume are accepted only while the download transfer is active, and retain the installing phase with download.paused indicating the state. Other overlapping or inapplicable commands return 409. Malformed commands return 400, invalid origin/cookie returns 403 and unsupported methods return 405. The API accepts no CLI options or arbitrary URLs. It trusts local processes as the rest of Loop does; the cookie is not a multi-user authentication system.

Entry uses GET with the query parameter refresh=1 to verify installation before returning the remembered sign-in and current host state. Ordinary GET reads only memory. Both responses disable HTTP caching and set the local control cookie. Refresh preserves a running host and its address, including when a probe fails. Stop and shutdown cancel a pending check; other actions return 409 until it completes. A failed probe or state-clear write returns HTTP 503 with the fixed code remote_status_failed, without CLI diagnostics. The frontend offers Retry; rechecking has no installation or authorization side effects, but clears the remembered sign-in when the executable is missing.

Errors distinguish unsupported platforms, download/storage failure, integrity failure, CLI operation failure, sign-in failure/timeout, sign-in-state storage failure, connection-preference storage failure and tunnel failure. Changing account requires an explicit action; application launch reconnects only when a previously successful connection left the preference enabled. After CLI readiness, Loop confirms cloud hostConnections for at least two seconds after the new process is at least four seconds old, with a forty-five-second verification deadline and eight-second probe timeouts. Connected hosts are checked every thirty seconds, including when timers resume after system sleep. A failed control API probe alone never tears down a working host. Two consecutive disconnected samples or an unexpected host exit trigger recovery using the same persisted tunnel identity and port. Recovery shows the connecting phase, closes stale ingress and hosting, and retries at one, two, four, eight, sixteen and thirty seconds. Each attempt must pass cloud verification. A minute of observed healthy hosting resets the retry budget. Six unsuccessful attempts report a sanitized tunnel failure for manual retry and retain the startup preference. Explicit Disconnect, Sign out and process shutdown abort probes and retry delays; recovery never installs the CLI or opens account sign-in.

Limits

Remote access uses the device-pairing policy and existing chat UI. Phone layout and PWA behavior are described in mobile layout and PWA; background delivery is not implemented. In development mode, remote ingress serves the last production frontend build; wait for the initial automatic frontend build before testing remote access. The Web build stages Vite output before publishing, replaces each entry atomically after its resources exist and publishes the service worker last. Failed builds keep the prior frontend active. Node builds preserve dist/web; old hashed resources remain available for open tabs. Use pnpm --filter @loop/web-ui build:frontend for frontend-only updates. Direct vite build bypasses staged publication. Old hashed assets are retained until the build output is explicitly cleaned while the server is stopped. Disconnected ingress returns 503. Automated tests use synthetic subprocess and service responses; they do not establish real account authorization, China connectivity or cross-platform execution.

Microsoft's managed service is in preview without a production SLA. Browser HTTPS ingress terminates TLS at the service; Loop does not promise encryption that hides traffic from the relay. The CLI is a separate downloaded executable, not an npm package or a self-hostable Microsoft relay. See CLI installation, service security, license terms and privacy statement.

Source and Tests