Skip to content

Remote Binding

Browser-approved device connection

When your service supports account-based device approval, copy its connection command:

aikito remote bind https://sync.example.com

The browser asks you to sign in and compare the connection code with your terminal. Only approve a command you started on your own device. The service selects your account's synchronization space automatically; credentials are generated locally and saved outside the workspace. After browser approval, the client also fetches and pins the same HTTPS origin's recipient public key before saving the device credential. Existing pins must still match; reconnecting never silently accepts a changed service key. This prepares local resource approval without granting resource access. No endpoint path, bearer token or public-key document needs to be copied.

Omit the URL to use AIKITO_REMOTE_SERVICE_URL or an interactive service URL prompt. Recovery codes are automatically saved outside the workspace to the private configuration file ~/.config/aikito/encrypted-remote/recovery.txt (XDG_CONFIG_HOME or %APPDATA% is honored). The file keeps one sync_id/code record per synchronization space, with service and workspace labels so multiple records can be distinguished. Files use owner-only permissions and updates are locked and atomic. Save a separate copy in case this device is lost. Another device signs in to the same account and enters that recovery code. Account passwords cannot decrypt synchronized data. Repeating the command on a configured workspace does not allocate new credentials; use --rebind explicitly when reconnecting after account reset.

Connection does not upload resources or start a background service. Preview with aikito remote sync --dry-run, then synchronize with aikito remote sync --apply. Explicit backend endpoints are reserved for protocol fixtures and custom services via --endpoint.

A remote binding pins one local workspace to a resource center. The endpoint locates it; sync_id identifies it. Reusing a URL does not authorize accepting a replacement center. Binding, authentication and endpoint routing remain outside Remote Protocol v1 and reconciliation.

Local state

workspace.remote_binding stores version-1 JSON at .local/state/aikito/workspace-reconcile/binding.json:

{
  "version": 1,
  "endpoint": "https://example.invalid/v1/remote",
  "sync_id": "0123456789abcdef0123456789abcdef",
  "auth": {"type": "bearer_env", "env": "AIKITO_REMOTE_TOKEN"}
}

Only the environment variable name is saved, never its secret value. Binding is local-only: it is excluded from resource snapshots, remote payloads and workspace Git context. It is separate from remote.json, which describes a filesystem resource center. Both the outer object and auth object reject unknown or missing fields, duplicate JSON keys, unsupported versions and malformed values. Identity uses the existing 32-character lowercase hexadecimal rule; environment names match [A-Z_][A-Z0-9_]*.

State readers validate path entries and never follow symlinks or reparse points. load_remote_binding is strictly read-only: it creates neither directories nor lock files, and does not recover journals. Atomic replacement lets readers see a complete old or new binding. Lifecycle writes use WorkspaceWriterLock, recover local resource journals before attachment checks and use the existing private state atomic writers and permission policy. Malformed state blocks creation and removal; it is never silently replaced.

Binding is deliberately absent from journal PathPolicy.states. Adding it would invalidate interrupted journals from earlier versions. Binding writes are independent of resource transactions and leave their compatibility policy intact.

Construction and identity

The internal workspace.remote_factory entry points are:

  • bind_remote(workspace, endpoint, auth, *, home=None, environ=None) reads the real remote identity and persists the pin under the writer lock. An existing binding is never overwritten. Existing replica or pending identities must match, including after an explicit unbind.
  • open_bound_remote(workspace, *, environ=None) loads the binding, resolves its credential and assembles HTTPTransport, SerializedRemoteStore and BoundRemote. Construction performs no network exchange or journal recovery.
  • verify_remote_binding(binding, *, environ=None) reads and validates the pin.
  • remove_remote_binding(workspace, *, home=None) explicitly removes only the binding. Unresolved pending blocks removal. Replica state, resources and remote contents are preserved. Removal does not authorize pairing a different center.

Future callers use this factory rather than assembling a bound transport stack themselves. These remain internal modules; no public CLI or package API is added.

Each bound instance verifies the pin before its first identity-bearing operation: fetch, commit and resolve_commit. recover also verifies first. Verification is cached only after a successful read. Every subsequent read still checks the returned identity; failure invalidates the cache. Expected snapshot and recovery identities must match the pin. A changed center raises StoreIdentityMismatch without updating binding or sending the old recovery identity to the replacement center. Backend snapshot and commit identity checks continue to apply after verification. Local attachment validation performs no network exchange. If the verification read reports RecoveryRequired, recover may run before the pin is verified: it carries no center, client or request identity and only completes the endpoint's own interrupted transaction. The pin is verified immediately after, before recover returns or any identity-bearing operation is sent. Thus resolve_commit reporting RecoveryRequired can still be recovered by pending recovery without exposing its recovery identity to an unverified center.

Credentials and access failures

The sole provider is bearer_env. The factory resolves a nonempty token consisting of visible ASCII characters without whitespace and explicitly provides the Authorization: Bearer ... value to HTTP. Transport never reads environment credentials. Credentials are excluded from object representations and exception messages. Authentication does not change protocol bytes or request digests.

Authenticated endpoints require HTTPS with normal certificate and hostname verification. Only exact 127.0.0.1, ::1 and localhost hosts may use HTTP for local testing. Embedded URL credentials and fragments are rejected. Credentials must never appear in query parameters; recognizable token, secret, password, credential, auth and API-key names are rejected. This name check is a guard, not permission to put secrets under another query name. Redirects are not followed.

The endpoint contract requires HTTP 401 and 403 to be returned before reading or dispatching a Remote Protocol operation. HTTP closes these responses without reading their bodies and raises TransportRejected. The adapter converts this to RemoteAccessDenied, a local StoreError, including for commit. Neither error is part of the protocol error-code mapping. 401 means authentication is required or invalid; 403 means authorization is denied. Other statuses, redirects, response loss and timeouts remain conservative transport failures: an uncertain commit raises CommitOutcomeUnknown.

Pending recovery and verification

Binding, credential and access failures retain the exact pending record. Even a contracted 401/403 does not clear it: intermediary rejection must not erase recovery evidence. Restarting with the same pin and valid credentials resolves the original request before reconciliation plans another mutation. An identity mismatch stops before lookup or backend recovery. Only existing local completion or SnapshotExpired paths clear pending.

Unit tests cover strict state validation, safe paths, identity-first calls and failure classification. The test HTTP bridge counts protocol dispatches and checks credentials before reading request bodies. An external implementation's black-box auth contract checks access rejection, absent receipt and unchanged revision, without requiring internal counters or a control endpoint.

AIKITO_REMOTE_TEST_SERVER_CMD supports {root} and {token} argv placeholders. The latter supplies an ephemeral test token; production credentials must not be placed in command arguments. To verify an auth-aware external implementation:

PYTHONPATH=src AIKITO_REMOTE_TEST_SERVER_CMD='python tests/http_remote_server.py --root {root} --token {token}' \
  python -m pytest tests/test_workspace_http_external.py -k external_auth_contract

Three-platform smoke jobs run tests/workspace_remote_binding_smoke.py with real HTTP sockets, assert binding and resource files, check that binding excludes the token, and verify pending retention, no unauthorized revision advance and restart recovery after lost responses. Existing recovery contracts and byte-exact protocol vectors continue to apply unchanged.

Independent server acceptance

The multi-store test assembly keeps URL store_id separate from the pinned protocol sync_id. A service restart reuses its listening address and store metadata, so a bound client keeps both pins. The external server smoke exercises real reconciliation, a lost accepted response, pending retention, service restart, and durable receipt resolution through open_bound_remote. It verifies that binding contains only the credential source and that successful recovery clears pending without a second publication. See the HTTP test assembly.