Skip to main content

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?

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

PlatformDiscovery Method
macOSdns-sd CLI tool (native)
Linuxavahi-browse (Avahi daemon)

TXT Record Fields

Each beacon advertises metadata via DNS TXT records:
FieldDescriptionExample
displayNameHuman-readable gateway name”Jason’s Mac Studio”
lanHostLAN hostname or IP”192.168.1.100”
tailnetDnsTailscale DNS name”mac-studio.tail12345.ts.net”
gatewayPortGateway WebSocket port”18789”
sshPortSSH port for remote access”22”
gatewayTlsWhether TLS is enabled”true”
gatewayTlsSha256TLS certificate fingerprintSHA-256 hash
cliPathPath to argent CLI on the host”/Users/sem/bin/argent”
roleGateway role”primary”
transportTransport 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:
1

Query Tailscale

Query tailscale status --json for known peer IPs
2

Probe Peers

Probe each Tailnet IP for PTR records matching _argent-gw._tcp
3

Resolve Records

Resolve SRV and TXT records from the responding nameserver
4

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/:
FileContents
pending.jsonPending pairing requests (TTL: 5 minutes)
paired.jsonApproved 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:
  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
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

OperationFunctionDescription
VerifyverifyDeviceToken()Check token validity, role, and scopes
EnsureensureDeviceToken()Get or create token for a role
RotaterotateDeviceToken()Generate new token, preserving history
RevokerevokeDeviceToken()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

ReasonDescription
device-not-pairedDevice ID not found in paired devices
role-missingNo role specified
token-missingNo token for the requested role
token-revokedToken has been revoked
token-mismatchToken value does not match
scope-mismatchRequested 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.