Skip to content

Commit 3bafc24

Browse files
authored
fix: support explicit remote daemon endpoints (#189)
* fix: support remote daemon base url * fix: harden remote daemon endpoint handling * docs: update remote daemon skill guidance
1 parent 9ca4e07 commit 3bafc24

10 files changed

Lines changed: 339 additions & 36 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ Flags:
236236
- `--activity <component>` (Android app launch only; package/Activity or package/.Activity; not for URL opens)
237237
- `--session <name>`
238238
- `--state-dir <path>` daemon state directory override (default: `~/.agent-device`)
239+
- `--daemon-base-url <url>` explicit remote HTTP daemon base URL; skips local daemon discovery/startup
240+
- `--daemon-auth-token <token>` remote HTTP daemon auth token; sent in both the JSON-RPC request token and HTTP auth headers (`Authorization: Bearer` and `x-agent-device-token`)
239241
- `--daemon-transport auto|socket|http` daemon client transport preference
240242
- `--daemon-server-mode socket|http|dual` daemon server mode (`http` and `dual` expose JSON-RPC over HTTP at `/rpc`)
241243
- `--tenant <id>` tenant identifier used with session isolation
@@ -514,6 +516,8 @@ Environment selectors:
514516
- `AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS=<ms>` to adjust iOS simulator boot timeout (default: `120000`, minimum: `5000`).
515517
- `AGENT_DEVICE_DAEMON_TIMEOUT_MS=<ms>` to override daemon request timeout (default `90000`). Increase for slow physical-device setup (for example `120000`).
516518
- `AGENT_DEVICE_STATE_DIR=<path>` override daemon state directory (metadata, logs, session artifacts).
519+
- `AGENT_DEVICE_DAEMON_BASE_URL=http(s)://host:port[/base-path]` connect directly to a remote HTTP daemon and skip local daemon metadata/startup.
520+
- `AGENT_DEVICE_DAEMON_AUTH_TOKEN=<token>` auth token for remote HTTP daemon mode; sent in both the JSON-RPC request token and HTTP auth headers (`Authorization: Bearer` and `x-agent-device-token`).
517521
- `AGENT_DEVICE_DAEMON_SERVER_MODE=socket|http|dual` daemon server mode. `http` and `dual` expose JSON-RPC 2.0 at `POST /rpc` (`GET /health` available for liveness).
518522
- `AGENT_DEVICE_DAEMON_TRANSPORT=auto|socket|http` client preference when connecting to daemon metadata.
519523
- `AGENT_DEVICE_HTTP_AUTH_HOOK=<module-path>` optional HTTP auth hook module path for JSON-RPC server mode.

skills/agent-device/SKILL.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Use this skill as a router, not a full manual.
2525
- Normal UI task: `open` -> `snapshot -i` -> `press/fill` -> `diff snapshot -i` -> `close`
2626
- Debug/crash: `open <app>` -> `logs clear --restart` -> reproduce -> `network dump` -> `logs path` -> targeted `grep`
2727
- Replay drift: `replay -u <path>` -> verify updated selectors
28-
- Remote multi-tenant run: allocate lease -> run commands with tenant isolation flags -> heartbeat/release lease
28+
- Remote multi-tenant run: allocate lease -> point client at remote daemon base URL -> run commands with tenant isolation flags -> heartbeat/release lease
2929
- Device-scope isolation run: set iOS simulator set / Android allowlist -> run selectors within scope only
3030

3131
## Canonical Flows
@@ -62,31 +62,40 @@ agent-device replay -u ./session.ad
6262
### 4) Remote Tenant Lease Flow (HTTP JSON-RPC)
6363

6464
```bash
65+
# Client points directly at the remote daemon HTTP base URL.
66+
export AGENT_DEVICE_DAEMON_BASE_URL=http://mac-host.example:4310
67+
export AGENT_DEVICE_DAEMON_AUTH_TOKEN=<token>
68+
6569
# Allocate lease
66-
curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
70+
curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
6771
-H "content-type: application/json" \
6872
-H "Authorization: Bearer <token>" \
6973
-d '{"jsonrpc":"2.0","id":"alloc-1","method":"agent_device.lease.allocate","params":{"runId":"run-123","tenantId":"acme","ttlMs":60000}}'
7074

7175
# Use lease in tenant-isolated command execution
72-
agent-device --daemon-transport http \
76+
agent-device \
7377
--tenant acme \
7478
--session-isolation tenant \
7579
--run-id run-123 \
7680
--lease-id <lease-id> \
7781
session list --json
7882

7983
# Heartbeat and release
80-
curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
84+
curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
8185
-H "content-type: application/json" \
8286
-H "Authorization: Bearer <token>" \
8387
-d '{"jsonrpc":"2.0","id":"hb-1","method":"agent_device.lease.heartbeat","params":{"leaseId":"<lease-id>","ttlMs":60000}}'
84-
curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
88+
curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
8589
-H "content-type: application/json" \
8690
-H "Authorization: Bearer <token>" \
8791
-d '{"jsonrpc":"2.0","id":"rel-1","method":"agent_device.lease.release","params":{"leaseId":"<lease-id>"}}'
8892
```
8993

94+
Notes:
95+
- `AGENT_DEVICE_DAEMON_BASE_URL` makes the CLI skip local daemon discovery/startup and call the remote HTTP daemon directly.
96+
- `AGENT_DEVICE_DAEMON_AUTH_TOKEN` is sent in both the JSON-RPC request token and HTTP auth headers.
97+
- In remote daemon mode, `--debug` does not tail a local `daemon.log`; inspect logs on the remote host instead.
98+
9099
## Command Skeleton (Minimal)
91100

92101
### Session and navigation
@@ -208,14 +217,15 @@ agent-device batch --steps-file /tmp/batch-steps.json --json
208217
- Use short lease TTLs and heartbeat only while work is active; release leases immediately after run completion/failure.
209218
- Env equivalents for scoped runs: `AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET` (compat `IOS_SIMULATOR_DEVICE_SET`) and
210219
`AGENT_DEVICE_ANDROID_DEVICE_ALLOWLIST` (compat `ANDROID_DEVICE_ALLOWLIST`).
220+
- For explicit remote client mode, prefer `AGENT_DEVICE_DAEMON_BASE_URL` / `--daemon-base-url` instead of relying on local daemon metadata or loopback-only ports.
211221

212222
## Security and Trust Notes
213223

214224
- Prefer a preinstalled `agent-device` binary over on-demand package execution.
215225
- If install is required, pin an exact version (for example: `npx --yes agent-device@<exact-version> --help`).
216226
- Signing/provisioning environment variables are optional, sensitive, and only for iOS physical-device setup.
217227
- Logs/artifacts are written under `~/.agent-device`; replay scripts write to explicit paths you provide.
218-
- For remote daemon mode, prefer `AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual` with `AGENT_DEVICE_HTTP_AUTH_HOOK` and tenant-scoped lease admission.
228+
- For remote daemon mode, prefer `AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual` on the host plus client-side `AGENT_DEVICE_DAEMON_BASE_URL`, with `AGENT_DEVICE_HTTP_AUTH_HOOK` and tenant-scoped lease admission where needed.
219229
- Keep logging off unless debugging and use least-privilege/isolated environments for autonomous runs.
220230

221231
## Common Mistakes

skills/agent-device/references/remote-tenancy.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ tenant/run admission control.
66
## Transport prerequisites
77

88
- Start daemon in HTTP mode (`AGENT_DEVICE_DAEMON_SERVER_MODE=http|dual`).
9-
- Use a token from daemon metadata or `Authorization: Bearer <token>`.
9+
- Point remote clients at the host with `AGENT_DEVICE_DAEMON_BASE_URL=http(s)://host:port[/base-path]`
10+
or `--daemon-base-url <url>` so the CLI skips local daemon discovery/startup.
11+
- Use `AGENT_DEVICE_DAEMON_AUTH_TOKEN` / `--daemon-auth-token` when the client should send the
12+
shared daemon token automatically.
13+
- Direct JSON-RPC callers can use a token in params, `Authorization: Bearer <token>`, or
14+
`x-agent-device-token`.
1015
- Prefer an auth hook (`AGENT_DEVICE_HTTP_AUTH_HOOK`) for caller validation and
1116
tenant injection.
1217

@@ -21,7 +26,7 @@ Use `POST /rpc` with JSON-RPC 2.0 methods:
2126
Example allocate:
2227

2328
```bash
24-
curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
29+
curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
2530
-H "content-type: application/json" \
2631
-H "Authorization: Bearer <token>" \
2732
-d '{"jsonrpc":"2.0","id":"alloc-1","method":"agent_device.lease.allocate","params":{"tenantId":"acme","runId":"run-123","ttlMs":60000}}'
@@ -30,7 +35,7 @@ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
3035
Example heartbeat:
3136

3237
```bash
33-
curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
38+
curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
3439
-H "content-type: application/json" \
3540
-H "Authorization: Bearer <token>" \
3641
-d '{"jsonrpc":"2.0","id":"hb-1","method":"agent_device.lease.heartbeat","params":{"leaseId":"<lease-id>","ttlMs":60000}}'
@@ -39,7 +44,7 @@ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
3944
Example release:
4045

4146
```bash
42-
curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
47+
curl -sS "${AGENT_DEVICE_DAEMON_BASE_URL}/rpc" \
4348
-H "content-type: application/json" \
4449
-H "Authorization: Bearer <token>" \
4550
-d '{"jsonrpc":"2.0","id":"rel-1","method":"agent_device.lease.release","params":{"leaseId":"<lease-id>"}}'
@@ -50,7 +55,7 @@ curl -sS http://127.0.0.1:${AGENT_DEVICE_DAEMON_HTTP_PORT}/rpc \
5055
For tenant-isolated command execution, pass all four flags:
5156

5257
```bash
53-
agent-device --daemon-transport http \
58+
agent-device \
5459
--tenant acme \
5560
--session-isolation tenant \
5661
--run-id run-123 \
@@ -60,6 +65,9 @@ agent-device --daemon-transport http \
6065

6166
Admission checks require tenant/run/lease scope alignment.
6267

68+
The CLI sends `AGENT_DEVICE_DAEMON_AUTH_TOKEN` in both the JSON-RPC request token field and HTTP
69+
auth headers so existing daemon auth paths continue to work.
70+
6371
## Failure semantics
6472

6573
- Missing tenant/run/lease fields in tenant isolation mode: `INVALID_ARGS`
@@ -70,6 +78,8 @@ Admission checks require tenant/run/lease scope alignment.
7078

7179
- Keep TTL short and heartbeat only while a run is active.
7280
- Release lease immediately on run completion/error paths.
81+
- For remote debug sessions, inspect logs on the remote host; client-side `--debug` no longer tails
82+
a local daemon log when `AGENT_DEVICE_DAEMON_BASE_URL` is set.
7383
- For bounded hosts, configure:
7484
- `AGENT_DEVICE_MAX_SIMULATOR_LEASES`
7585
- `AGENT_DEVICE_LEASE_TTL_MS`

src/__tests__/cli-diagnostics.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import test from 'node:test';
22
import assert from 'node:assert/strict';
3+
import fs from 'node:fs';
4+
import os from 'node:os';
5+
import path from 'node:path';
36
import { runCli } from '../cli.ts';
47
import type { DaemonRequest, DaemonResponse } from '../daemon-client.ts';
8+
import { resolveDaemonPaths } from '../daemon/config.ts';
59

610
class ExitSignal extends Error {
711
public readonly code: number;
@@ -76,6 +80,33 @@ test('cli forwards --debug as verbose/debug metadata', async () => {
7680
assert.equal(typeof result.calls[0]?.meta?.requestId, 'string');
7781
});
7882

83+
test('cli does not tail local daemon log when remote daemon base URL is set', async () => {
84+
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-cli-remote-'));
85+
const daemonPaths = resolveDaemonPaths(stateDir);
86+
fs.mkdirSync(path.dirname(daemonPaths.logPath), { recursive: true });
87+
fs.writeFileSync(daemonPaths.logPath, 'REMOTE_TAIL_SENTINEL\n', 'utf8');
88+
89+
const previousBaseUrl = process.env.AGENT_DEVICE_DAEMON_BASE_URL;
90+
process.env.AGENT_DEVICE_DAEMON_BASE_URL = 'http://remote-mac.example.test:7777/agent-device';
91+
92+
try {
93+
const result = await runCliCapture(['clipboard', 'write', 'hello', '--debug', '--state-dir', stateDir], async () => {
94+
await new Promise((resolve) => setTimeout(resolve, 300));
95+
return {
96+
ok: true,
97+
data: { action: 'write' },
98+
};
99+
});
100+
assert.equal(result.code, null);
101+
assert.equal(result.stdout.includes('REMOTE_TAIL_SENTINEL'), false);
102+
assert.match(result.stdout, /Clipboard updated/);
103+
} finally {
104+
if (previousBaseUrl === undefined) delete process.env.AGENT_DEVICE_DAEMON_BASE_URL;
105+
else process.env.AGENT_DEVICE_DAEMON_BASE_URL = previousBaseUrl;
106+
fs.rmSync(stateDir, { recursive: true, force: true });
107+
}
108+
});
109+
79110
test('cli returns normalized JSON failures with diagnostics fields', async () => {
80111
const result = await runCliCapture(['open', 'settings', '--json'], async () => ({
81112
ok: false,

src/cli.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS):
9999
const daemonFlags = toDaemonFlags(flags);
100100
const daemonPaths = resolveDaemonPaths(flags.stateDir ?? process.env.AGENT_DEVICE_STATE_DIR);
101101
const sessionName = flags.session ?? process.env.AGENT_DEVICE_SESSION ?? 'default';
102-
const logTailStopper = flags.verbose && !flags.json ? startDaemonLogTail(daemonPaths.logPath) : null;
102+
const remoteDaemonBaseUrl = flags.daemonBaseUrl ?? process.env.AGENT_DEVICE_DAEMON_BASE_URL;
103+
const logTailStopper = flags.verbose && !flags.json && !remoteDaemonBaseUrl
104+
? startDaemonLogTail(daemonPaths.logPath)
105+
: null;
103106
const sendDaemonRequest = async (payload: { command: string; positionals: string[]; flags?: Record<string, unknown> }) =>
104107
await deps.sendToDaemon({
105108
session: sessionName,

0 commit comments

Comments
 (0)