> ## Documentation Index
> Fetch the complete documentation index at: https://docs.argentos.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Device Pairing & Discovery

> Bonjour service discovery and device registration for multi-device agent access across the local network and Tailnet.

## Overview

Device Pairing & Discovery enables ArgentOS to operate across multiple devices on the same network. The system uses Bonjour (mDNS/DNS-SD) for automatic gateway discovery and a structured pairing workflow for device registration with role-based access tokens.

This system answers two questions:

1. **Discovery**: Where is the ArgentOS gateway running on this network?
2. **Pairing**: Is this device authorized to connect?

```mermaid theme={null}
sequenceDiagram
    participant Device as Device (Mac Mini, iPhone)
    participant Gateway as Gateway (primary device)
    Device->>Gateway: 1. Bonjour Browse (_argent-gw._tcp)
    Gateway-->>Device: 2. Resolve host, port, TXT records
    Device->>Gateway: 3. Pairing Request (deviceId + publicKey)
    Gateway->>Gateway: 4. Operator approves on primary device
    Gateway-->>Device: 5. Role-scoped auth token
```

## Bonjour Service Discovery

### Service Type

ArgentOS gateways advertise as `_argent-gw._tcp` via mDNS/DNS-SD. This allows any device on the local network to discover running gateway instances without knowing their IP addresses.

### Platform Support

| Platform  | Discovery Method              |
| --------- | ----------------------------- |
| **macOS** | `dns-sd` CLI tool (native)    |
| **Linux** | `avahi-browse` (Avahi daemon) |

### TXT Record Fields

Each beacon advertises metadata via DNS TXT records:

| Field              | Description                    | Example                       |
| ------------------ | ------------------------------ | ----------------------------- |
| `displayName`      | Human-readable gateway name    | "Jason's Mac Studio"          |
| `lanHost`          | LAN hostname or IP             | "192.168.1.100"               |
| `tailnetDns`       | Tailscale DNS name             | "mac-studio.tail12345.ts.net" |
| `gatewayPort`      | Gateway WebSocket port         | "18789"                       |
| `sshPort`          | SSH port for remote access     | "22"                          |
| `gatewayTls`       | Whether TLS is enabled         | "true"                        |
| `gatewayTlsSha256` | TLS certificate fingerprint    | SHA-256 hash                  |
| `cliPath`          | Path to argent CLI on the host | "/Users/sem/bin/argent"       |
| `role`             | Gateway role                   | "primary"                     |
| `transport`        | Transport protocol             | "ws"                          |

### Discovery Domains

Discovery searches multiple domains:

1. **`local.`** -- Standard mDNS local network discovery
2. **Wide-area domain** -- Optional Tailnet DNS for cross-network discovery

### Tailnet Fallback

When standard Bonjour discovery fails for wide-area domains, the system falls back to a Tailnet-based DNS approach:

<Steps>
  <Step title="Query Tailscale">
    Query `tailscale status --json` for known peer IPs
  </Step>

  <Step title="Probe Peers">
    Probe each Tailnet IP for PTR records matching `_argent-gw._tcp`
  </Step>

  <Step title="Resolve Records">
    Resolve SRV and TXT records from the responding nameserver
  </Step>

  <Step title="Concurrent Probing">
    Concurrent probing (6 workers) with timeout budgets
  </Step>
</Steps>

This enables gateway discovery across Tailscale networks where mDNS may not propagate.

### Discovery API

```typescript theme={null}
import { discoverGatewayBeacons } from "./infra/bonjour-discovery.ts";

const beacons = await discoverGatewayBeacons({
  timeoutMs: 2000,           // Discovery timeout
  domains: ["local."],       // Search domains
  wideAreaDomain: null,      // Optional Tailnet domain
});

// Returns GatewayBonjourBeacon[]
// Each beacon has: instanceName, host, port, displayName, txt, etc.
```

## Device Pairing

### Pairing State

Device pairing state is persisted to `~/.argentos/devices/`:

| File           | Contents                                  |
| -------------- | ----------------------------------------- |
| `pending.json` | Pending pairing requests (TTL: 5 minutes) |
| `paired.json`  | Approved paired devices                   |

<Note>
  Both files are written atomically (write to temp, rename) with `0o600` permissions for security.
</Note>

### Pairing Request

A device initiates pairing by sending a request with its identity:

```typescript theme={null}
interface DevicePairingPendingRequest {
  requestId: string;          // UUID
  deviceId: string;           // Unique device identifier
  publicKey: string;          // Device public key
  displayName?: string;       // "Jason's iPhone"
  platform?: string;          // "ios", "macos", "android"
  clientId?: string;          // Client application ID
  clientMode?: string;        // Client mode
  role?: string;              // Requested role
  roles?: string[];           // Requested roles
  scopes?: string[];          // Requested permission scopes
  remoteIp?: string;          // Source IP
  silent?: boolean;           // Suppress notification
  isRepair?: boolean;         // Re-pairing existing device
  ts: number;                 // Request timestamp
}
```

<Warning>
  Pending requests expire after 5 minutes if not approved.
</Warning>

### Approval

The operator approves pairing on the primary device. On approval:

1. The pending request is removed
2. A paired device record is created
3. A role-scoped auth token is generated
4. If the device was previously paired, roles and scopes are merged

```typescript theme={null}
const result = await approveDevicePairing(requestId);
// result.device contains the PairedDevice with auth tokens
```

### Rejection

```typescript theme={null}
await rejectDevicePairing(requestId);
// Pending request is removed, no device record created
```

### Paired Device

```typescript theme={null}
interface PairedDevice {
  deviceId: string;
  publicKey: string;
  displayName?: string;
  platform?: string;
  clientId?: string;
  role?: string;
  roles?: string[];
  scopes?: string[];
  tokens?: Record<string, DeviceAuthToken>;
  createdAtMs: number;
  approvedAtMs: number;
}
```

## Auth Tokens

Each paired device receives role-scoped auth tokens for API access.

### Token Structure

```typescript theme={null}
interface DeviceAuthToken {
  token: string;              // 32-char hex token (UUID without dashes)
  role: string;               // Token role (e.g., "client", "admin")
  scopes: string[];           // Permitted operations
  createdAtMs: number;
  rotatedAtMs?: number;       // Last rotation timestamp
  revokedAtMs?: number;       // Revocation timestamp (if revoked)
  lastUsedAtMs?: number;      // Last successful verification
}
```

### Token Operations

| Operation  | Function              | Description                            |
| ---------- | --------------------- | -------------------------------------- |
| **Verify** | `verifyDeviceToken()` | Check token validity, role, and scopes |
| **Ensure** | `ensureDeviceToken()` | Get or create token for a role         |
| **Rotate** | `rotateDeviceToken()` | Generate new token, preserving history |
| **Revoke** | `revokeDeviceToken()` | Mark token as revoked                  |

### Scope Enforcement

Token verification checks that requested scopes are a subset of the token's allowed scopes:

```typescript theme={null}
const result = await verifyDeviceToken({
  deviceId: "jason-iphone",
  token: "a1b2c3d4...",
  role: "client",
  scopes: ["chat", "tasks"],
});
// { ok: true } or { ok: false, reason: "scope-mismatch" }
```

### Verification Failure Reasons

| Reason              | Description                                    |
| ------------------- | ---------------------------------------------- |
| `device-not-paired` | Device ID not found in paired devices          |
| `role-missing`      | No role specified                              |
| `token-missing`     | No token for the requested role                |
| `token-revoked`     | Token has been revoked                         |
| `token-mismatch`    | Token value does not match                     |
| `scope-mismatch`    | Requested scopes exceed token's allowed scopes |

## Concurrency Safety

All pairing operations use a serialized lock to prevent race conditions:

```typescript theme={null}
async function withLock<T>(fn: () => Promise<T>): Promise<T> {
  const prev = lock;
  lock = new Promise(resolve => { release = resolve; });
  await prev;
  try { return await fn(); }
  finally { release?.(); }
}
```

This ensures atomic state transitions even when multiple pairing requests arrive simultaneously.

## Re-Pairing

When an already-paired device sends a new pairing request, the system detects this and sets `isRepair: true`. On approval, the existing device record is updated (roles and scopes are merged) rather than replaced.

## Configuration

Device pairing state is stored under the ArgentOS state directory:

```
~/.argentos/
└── devices/
    ├── pending.json    # Pending requests (auto-expire after 5 min)
    └── paired.json     # Approved devices with tokens
```

<Info>
  No configuration is needed to enable device pairing -- it is available by default when the gateway is running.
</Info>
