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:
- Discovery: Where is the ArgentOS gateway running on this network?
- Pairing: Is this device authorized to connect?
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 | 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:
local. — Standard mDNS local network discovery
- 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:
Query Tailscale
Query tailscale status --json for known peer IPs
Probe Peers
Probe each Tailnet IP for PTR records matching _argent-gw._tcp
Resolve Records
Resolve SRV and TXT records from the responding nameserver
Concurrent Probing
Concurrent probing (6 workers) with timeout budgets
This enables gateway discovery across Tailscale networks where mDNS may not propagate.
Discovery API
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 |
Both files are written atomically (write to temp, rename) with 0o600 permissions for security.
Pairing Request
A device initiates pairing by sending a request with its identity:
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
}
Pending requests expire after 5 minutes if not approved.
Approval
The operator approves pairing on the primary device. On approval:
- The pending request is removed
- A paired device record is created
- A role-scoped auth token is generated
- If the device was previously paired, roles and scopes are merged
const result = await approveDevicePairing(requestId);
// result.device contains the PairedDevice with auth tokens
Rejection
await rejectDevicePairing(requestId);
// Pending request is removed, no device record created
Paired Device
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
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:
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:
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
No configuration is needed to enable device pairing — it is available by default when the gateway is running.