Agent Uplink

DEVELOPMENT DOCUMENTATION / SETUP

Install and connect an enrolled computer.

An operator-managed setup using installed Agent Uplink CLI binaries and a provisioned relay environment.

Joining a relay someone else runs? This page is for operators. Follow Join a relay to install the device half, get a certificate from your operator, connect and call the echo.

Current main includes first-time catalog provisioning for one tenant, user, device credential, service and grant in a fresh namespace. A single-relay synthetic echo path has scoped local evidence; you still supply TLS Redis, listener certificates and identity/certificate issuers. Cluster membership publishing and the HTTPS checkpoint authority remain unshipped. Older binaries may not have the provisioning commands.

1. Prepare the environment

Use Linux or macOS for the relay host. Windows is device-only: install its matching tunnel-client.exe and tunnel-deadman.exe together. Run the relay configuration, provisioning and recovery commands below on the separate Unix relay host, not the Windows device.

2. Check the installed CLI

Checking published versions...

View GitHub releases

On the device:

tunnel-client --help

On the Linux or macOS relay host:

tunnel-relay --help

The product name changed; the tunnel-client and tunnel-relay binary names remain unchanged. Windows credential creation and import are unsupported; the credential workflow below requires a Unix device host. Shipping Windows device binaries is not proof of a supported end-to-end enrollment path.

In releases up to f9f7abf, the tunnel-client --help heading still says "M1 connector" and calls rotation and resume future work; the client does rotate and reconnect. Subcommands do not accept --help: tunnel-client connect --help prints the usage and exits 2.

3. Configure the relay

Use examples/m1-relay.toml as a schema reference, not a ready deployment. Replace its placeholder values and certificate paths. Keep private material outside source control.

ConfigurationRequired operator decision
consumer_bind / device_bindSeparate listener addresses; the device listener requires client certificates.
oidc_issuer / oidc_audience / oidc_jwks_pathYour consumer identity issuer, accepted audience and verification key file.
redis_url / redis_namespaceTLS Redis endpoint and a durable namespace. The namespace accepts 1–96 ASCII letters, digits, periods, underscores or hyphens.
TLS certificate, key and CA pathsCorrect listener identities and the device certificate trust boundary.
node_id / deployment_incarnationKeep the coordinated relay identity and deployment incarnation. Do not configure boot_id: each relay process generates a fresh one. Remove the example boot_id line; a cluster configuration that sets it is refused by check-serve-config. Do not change the deployment incarnation to bypass a recovery error.

oidc_jwks_path is a local JSON key set, not a JWKS URL. The relay accepts RSA keys with a non-empty kid, n and e, and alg set to RS256 or omitted. Key rotation requires updating that file and restarting the relay. Tokens need RS256, a known key ID, an exactly matching issuer (including trailing slash), an accepted audience, a non-empty subject and valid expiry/not-before times with no clock leeway. Scope is a space-separated string. No issuer or token-generation tool is shipped.

tunnel-relay \
  check-serve-config --config /absolute/path/relay.toml

This dry run checks the serving configuration document. It does not read referenced credentials, connect to Redis, verify the deployment or provision authority records. The legacy check-config validates a different configuration type.

4. Configure the device

Start from examples/m1-client.toml. Resolve certificate paths relative to the client configuration file. Replace the device ID, relay URL and all credential references.

device_id = "your-enrolled-device"
relay_url = "wss://relay.example.test/v1/tunnel/control"

[credentials]
client_certificate = "credentials/device-cert-chain.pem"
client_key = "credentials/device-key.pem"
server_ca = "credentials/relay-ca.pem"

This excerpt is not a complete configuration. Keep the example's required limits and rotation sections. Its synthetic echo export is for a controlled transport smoke test, not filesystem, MCP, ACP or desktop access.

5. Provision and validate credentials

tunnel-client \
  config check --config /absolute/path/client.toml

tunnel-client \
  credentials create --config /absolute/path/client.toml \
  --csr-out /absolute/path/device.csr

Have your operator sign the CSR with the approved device issuer and establish the matching catalog identity and grants. Creating a CSR does not enroll or authorize a device.

tunnel-client \
  credentials import --config /absolute/path/client.toml \
  --certificate /absolute/path/device-cert-chain.pem \
  --server-ca /absolute/path/relay-ca.pem

tunnel-client \
  doctor --config /absolute/path/client.toml --json

doctor performs local checks. A network doctor, enrollment-code workflow and managed credential renewal are not delivered commands in this reviewed CLI.

Current main requires an X.509 v3 device certificate with a device-role URI SAN matching the profile's device UUID. credentials import reports a missing or wrong SAN separately from a pending-key mismatch and refuses the invalid certificate before installation.

Current main keeps result present even on configuration or credential failure, including host capability details. Each check reports its own status: not_run is distinct from failed. Older builds may return result: null; that is not evidence that capabilities passed. Correct the reported error and rerun.

On Unix, PROCESS_CONTAINMENT_SENTINEL_MISSING means the helper was not found; PROCESS_CONTAINMENT_SENTINEL_UNUSABLE means the candidate is not a regular file or the process lacks execute permission. Keep the genuine packaged tunnel-deadman beside the client and replace an invalid candidate with the matching helper. Both conditions report degraded containment without changing the doctor's verdict or exit code.

PROCESS_CONTAINMENT_SENTINEL_PRESENT establishes a regular file and kernel-granted execute permission, not helper identity or guaranteed startup. Doctor does not run the candidate. A later startup failure produces a separate warning; non-Unix process containment remains unsupported.

6. Provision a fresh catalog

Prepare a records document using examples/m6-catalog.toml from the matching source revision. CI archives ship only m1-client.toml and m1-relay.toml; take the catalog templates from https://github.com/andymac4182/agentuplink/tree/<sourceSha>/examples, with sourceSha from the archive's release.json. Its certificate path is relative to that document. The client device ID must be the catalog device UUID; its certificate needs URI SAN urn:agent-tunnel:device:<device UUID>, and its export table must use the catalog service UUID. Replace synthetic placeholders consistently.

Choose one initial service before provisioning. Current main accepts echo, http-forward for MCP or ACP, and fs; other types, including computer use, are not supported by this command. Initial provisioning creates one service; use the catalog-change commands below for later additions, not another provision-catalog run.

ServiceRecords templateRequired fields and grants
MCPexamples/m6-catalog-mcp.tomlhttp_forward_profile = mcp-2025-11-25 or mcp-2026-07-28; http:invoke.
ACPexamples/m6-catalog-acp.tomlhttp_forward_profile = acp-http-v1; http:invoke.
Filesystemexamples/m6-catalog-fs.tomlfs_case_sensitivity = sensitive or insensitive-preserving; fs:connect plus at least one of fs:read, fs:list, fs:write or fs:delete.

For HTTP services, the relay configuration must enable the same profile in [http_forward] profiles; use that configuration for both provisioning and serving. The dry run refuses a missing or unsupported profile and invalid filesystem metadata or grants. The device export must match the service UUID and protocol, with its own backend or confined root. Example comments show the client export shape. Older provisioning builds omitted this metadata and could leave these services returning 404.

tunnel-relay provision-catalog --config /absolute/path/relay.toml --records /absolute/path/catalog.toml --dry-run

# The following commands write to the configured Redis namespace:
tunnel-relay activate-first-incarnation --config /absolute/path/relay.toml
tunnel-relay provision-catalog --config /absolute/path/relay.toml --records /absolute/path/catalog.toml

The dry run reads configuration, records and the certificate but contacts no Redis and writes nothing. Activation requires an empty namespace. Provision after activation, before serving. Current main refuses an activated but unprovisioned namespace before writing, with stage=authority_identity class=unprovisioned; older builds could create keys that blocked provisioning. The initial seed and reservation now run in one Redis script, with rollback on failure. Retry after correcting a failure only when rollback completed. An incomplete-rollback diagnostic means keys may remain: inspect the namespace rather than blindly retrying or deleting authority keys. An interrupted writing command can also have an unknown outcome. Provisioning is once per namespace, not an update or repair command; older partial namespaces are not automatically repaired. Do not bypass recovery.

7. Start the provisioned relay and device

tunnel-relay \
  serve --config /absolute/path/relay.toml

# On the enrolled computer, in a separate terminal:
tunnel-client \
  connect --config /absolute/path/client.toml --json

Service and grant operations use exact names such as echo:invoke. Provisioning and its dry run reject names containing *; wildcard grants are not supported.

The client runs in the foreground. Start only after catalog records, grants, identity and trust have been provisioned. Relay connectivity alone does not grant access to an export. Add capability-specific configuration only after checking its support boundaries.

8. Call the echo

With the device connected, call its echo service through the consumer listener. The token must name a catalog user with a grant for the service and carry echo:invoke in its scope. Pass the relay's CA if its certificate is not from a public CA.

read -rs TOKEN
curl --cacert /absolute/path/relay-ca.pem \
  -H "Authorization: Bearer $TOKEN" --data-binary hello \
  https://relay.example.test/v1/devices/DEVICE_UUID/services/SERVICE_UUID/echo

A working tunnel answers HTTP 200 with the export's device_canary followed by hello. 503 DEVICE_OFFLINE means the device is not connected; 401 means the token was missing or not accepted; 404 SERVICE_NOT_FOUND means a wrong service or no grant. Current main answers 403 for a valid token without the scope; older relays answer 401. Current main caches a bounded Redis authority check for single-relay /readyz; it is not instantaneous or an end-to-end check. Older builds lacked that dependency check. Use the echo to verify the complete path. The Join a relay page lists every answer.

After initial provisioning

Current main supplies seven tunnel-relay catalog-change commands for the same namespace while serve runs. Each takes the relay's --config and supports --dry-run. Older binaries may not include them.

CommandInput and effect
add-user--records USER.toml: [user] with tenant, id, display_name and oidc_subject; joins an existing tenant.
add-device--records DEVICE.toml: [device] with tenant, owner, id, display_name and certificate; owner must be an existing member.
add-service--records SERVICE.toml: [service] with tenant, device, id, type, display_name, operations and the required protocol metadata.
set-grant--records GRANT.toml: [grant] with tenant, user, device, service and operations; adds or replaces the grant and advances its revision.
revoke-grant--tenant UUID --user UUID --device UUID --service UUID: removes one grant.
revoke-device--tenant UUID --device UUID: revokes the device, its credentials and grants, and its owner lease.
revoke-credential--tenant UUID --device UUID --credential UUID: revokes one device credential.
tunnel-relay add-user --config /absolute/path/relay.toml --records /absolute/path/user.toml --dry-run
tunnel-relay add-device --config /absolute/path/relay.toml --records /absolute/path/device.toml --dry-run
tunnel-relay add-service --config /absolute/path/relay.toml --records /absolute/path/service.toml --dry-run
tunnel-relay set-grant --config /absolute/path/relay.toml --records /absolute/path/grant.toml --dry-run

Prepare separate records documents using docs/operator.md section 2.5 from the matching source revision. Do not pass an entire initial-provisioning document to these commands. Certificate paths are relative to the records document. Dry runs validate record shape, certificate identity and service rules without contacting Redis; they cannot confirm existing tenants, membership, duplicates or grant compatibility with stored services. Remove --dry-run only when ready to write, in dependency order: user, device, service, grant.

Each catalog change uses a single Redis script and checks the active incarnation. Additions reject duplicate identities rather than overwriting them; set-grant deliberately replaces an existing grant and refuses a revoked device. Keep the credential ID printed by add-device: no command lists credentials. Device export names and local policies must still match the service records; a catalog change does not reload the client's configuration.

New records and grants affect the next connection or request without a relay restart. Grant revocation refuses subsequent requests, but already admitted work can retain its authorization for up to five seconds. Device and credential revocation also close live sessions during maintenance. Scope and timing limits are on the support page. If a command is interrupted with an unknown outcome, reconcile state before retrying; atomic execution does not guarantee the caller received its result.

Still unsupported: adding a second tenant; changing or deactivating users or memberships; changing service types or operations; reactivating revoked devices or credentials; and renewing a certificate for the same device. A replacement can use a new device UUID, key and certificate, followed by its service and grant, then revocation of the old device. That keeps the namespace but changes device/export identities; it is not in-place renewal.

Reconnect and service management

Current main's foreground connect retries reconnectable failures by default, with exponential delay ceilings from 1 to 60 seconds and jitter between half and all of each ceiling. Each reconnect starts a fresh session, not a replay of interrupted operations. Invalid configuration and classified credential refusals are terminal; opaque protocol-version or cluster identity refusals may still be retried. Older builds exit on connection loss.

Known defect: after a network stall with a request in flight, the 2026-09-25 live smoke check saw connect exit 1 once in five runs with a non-retryable PROTOCOL_ERROR, "STREAM_FORGET terminal proof did not converge before its deadline", instead of reconnecting. Current main contains a fix (M6-C105, awaiting verification) that is not in releases up to f9f7abf; with those releases, run connect under a service manager that restarts it on exit 1.

# Optional client profile settings (defaults shown):
[reconnect]
enabled = true
initial_delay_ms = 1000
max_delay_ms = 60000
max_attempts = 0

# Let an external supervisor own all restarts instead:
# tunnel-client connect --config /absolute/path/client.toml --no-reconnect

max_attempts = 0 means no attempt limit. A finite value bounds consecutive retries; the count resets only after a session stays ready for the maximum delay. JSON lifecycle events report disconnected, backoff, reconnecting and reconnected. Stop and restart to load changed configuration. See support for the stale-owner network-change limitation.

The source tree's examples/service/ contains systemd client/relay units and a macOS launchd client example; these are not included in release bundles. Adjust paths, service accounts and stop timeouts. Repository evidence covers static checks for all examples and a limited live systemd client test; the relay unit was not live-tested and the launchd example was linted, not loaded. Windows service installation remains unsupported.

Upgrade by stopping, replacing the binaries from one matching platform bundle (three on Linux/macOS; client and deadman only on Windows), and starting with the same configuration. Keep device and relay versions matched across hosts. Preserve credentials, Redis catalog and relay state files. Sessions and supervised children restart; interrupted mutations can have unknown outcomes. Mixed-version deployments, downgrades and seamless rolling upgrades are not supported.

Process lifecycle

Current main handles SIGINT and SIGTERM on Unix during connect and serve. An orderly live-session or serving stop exits 0; interruption before readiness/startup completes or during reconnect backoff exits 130. A second stop request abandons the shutdown wait with exit 130. Client drain timeout also exits 130; its bound is the configured handshake timeout plus overlap. Relay serving drain has no overall deadline, so a service manager needs a stop timeout and escalation policy. Windows handles Ctrl-C only; SIGHUP is not handled. Older builds may lack this behavior.

For relay commands that write state, including activation, provisioning and recovery, the first stop request lets the command finish and report its outcome. A second request abandons it with exit 130 and an unknown outcome. Do not retry a potentially partial catalog write or reuse a consumed recovery approval blindly. Scoped local process tests and unit tests exist, but not every shutdown phase or platform is verified.

Single-relay Fly.io deployment

The source tree now includes deploy/fly/ Dockerfiles, entrypoints and configuration templates, plus the ordered runbook in docs/deploy-fly.md. This is an operator-managed alpha deployment, not a public relay service or a one-command installer. Use the guide and templates from the same reviewed source revision as your image.

The topology is one relay and one private, persistent TLS Redis instance in the same region. Both consumer HTTPS and device mTLS use TCP passthrough so the relay terminates TLS itself. Supply your own listener certificates, device trust and identity issuer; do not put credentials in images or source control. This configuration is not a cluster: do not scale either app above one machine or use a deployment strategy that overlaps two unclustered relays on the namespace.

Follow the runbook's order: build and push the image without starting the serving relay, validate configuration, activate the empty namespace, provision the catalog, then deploy that exact image. A serve started between activation and provisioning refuses the namespace (class=unprovisioned) and writes nothing; builds before M6-C34 did not, and could leave it unprovisionable. Use a build at or after 721ed2a for the corrected Redis startup connection budget. Provisioning still uses the installed tunnel-relay commands described above.

Plan downtime: relay replacement ends sessions. Current Fly templates enable the single-relay Redis continuity option, but an in-place restart must satisfy the durability and continuity checks described on the support page. A restore, replacement volume or failover is not covered. Current main's single-relay /readyz includes a cached Redis authority check; older deployments may lack it. Verify with an end-to-end echo. Machines, storage, addresses, traffic and remote builds may incur charges; check current provider pricing before creating resources.

Hosted upgrade evidence: the Fly relay has served main-77bfd28 since 2026-09-24; no published release is built from that commit, and clients from the 5c2d907 and f9f7abf pre-releases passed a live echo, rotation and reconnect smoke check against it on 2026-09-25. Earlier, the repository recorded an in-place upgrade of an already provisioned single relay to main-af23c2f on 2026-09-24, with Redis continuity enabled at 5 seconds. A matching Mac client completed 150 echoes in one session. After a SIGTERM relay restart, it reconnected automatically; the second echo attempt succeeded four seconds after the restart. These are recorded observations, not timing guarantees or a new test run here.

Evidence boundary: Redis persistence settings were checked on Fly, but a Fly Redis restart was not exercised. Local rehearsal covered Redis restart/crash and rollback; it does not establish hosted recovery or supported mixed-version operation. Fresh hosted activation and provisioning with a main-built image remain unverified; earlier hosted provisioning used a diagnostic image. The rollback target from main-77bfd28 is main-af23c2f, which keeps Redis continuity but refuses the catalog commands below; rolling back further to main-721ed2a also removes Redis continuity support. Full adapter and production acceptance remain separate.

Catalog commands on Fly: current main's image entrypoint accepts add-user, add-device, add-service, set-grant, revoke-grant, revoke-device and revoke-credential, including their dry runs. The serving main-77bfd28 image has this entrypoint; the older main-af23c2f image still refuses them. Use an image containing this entrypoint change for the workflow in docs/deploy-fly.md section 6.6.

Run each command in a one-off machine of the relay app using the same image as the serving relay, with records and certificates supplied through --file-local. The entrypoint supplies its configuration and refuses both --config PATH and --config=PATH; it passes command arguments unchanged. Do not run the entrypoint in a console on the serving machine: it rewrites the secrets directory that relay uses. One-off machines incur charges; destroy them afterwards and confirm only the serving relay remains, since leftovers can become extra relays on a later deployment.

Verification boundary: local container proof covers all seven dry runs, onboarding a second tester, echo access, revocations and entrypoint refusals while the relay serves. The repository now records a hosted add-user --dry-run on 2026-09-24 using main-77bfd28: the one-off machine exited 0, was destroyed, and only the serving relay remained. No catalog write command has been verified on Fly. A catalog dry run validates local inputs without contacting Redis or proving live authority access. This is recorded evidence, not a new hosted test; no Fly operations were performed for this website review.

Acceptance before deployment

A downloaded build is not a replacement for deployment-specific Redis/socket and capability acceptance. Cluster support and applicable adapter gates remain private-alpha requirements. Use dedicated disposable desktop VMs for computer-use tests. Developer build instructions are separate on the downloads page.

Source & review

Reviewed against cb94dc3ccbf4 on 2026-09-25. Based on examples/m1-client.toml, examples/m1-relay.toml, crates/tunnel-client/src/main.rs, docs/operator.md, docs/runtime.md, docs/deploy-fly.md, deploy/fly/, docs/tasks.md. Historical test evidence is not a new test run.