Skip to content

Commit b685a0b

Browse files
authored
Add remote HTTP daemon mode with lease admission controls (#136)
* fix: add daemon remote HTTP server mode with isolation controls * feat: add tenant lease lifecycle and admission control * refactor: simplify lease scope and rpc method routing * fix: correct smoke expectations for close without session
1 parent 803ba25 commit b685a0b

17 files changed

Lines changed: 1882 additions & 129 deletions

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,13 @@ Flags:
211211
- `--serial <serial>` (Android)
212212
- `--activity <component>` (Android app launch only; package/Activity or package/.Activity; not for URL opens)
213213
- `--session <name>`
214+
- `--state-dir <path>` daemon state directory override (default: `~/.agent-device`)
215+
- `--daemon-transport auto|socket|http` daemon client transport preference
216+
- `--daemon-server-mode socket|http|dual` daemon server mode (`http` and `dual` expose JSON-RPC over HTTP at `/rpc`)
217+
- `--tenant <id>` tenant identifier used with session isolation
218+
- `--session-isolation none|tenant` explicit session isolation mode (`tenant` scopes session namespace as `<tenant>:<session>`)
219+
- `--run-id <id>` run identifier used with tenant-scoped lease admission
220+
- `--lease-id <id>` active lease identifier used with tenant-scoped lease admission
214221
- `--count <n>` repeat count for `press`/`swipe`
215222
- `--interval-ms <ms>` delay between `press` iterations
216223
- `--hold-ms <ms>` hold duration per `press` iteration
@@ -262,7 +269,7 @@ Sessions:
262269
- If a session is already open, `open <app|url>` switches the active app or opens a deep link URL.
263270
- `close` stops the session and releases device resources. Pass an app to close it explicitly, or omit to just close the session.
264271
- Use `--session <name>` to manage multiple sessions.
265-
- Session scripts are written to `~/.agent-device/sessions/<session>-<timestamp>.ad` when recording is enabled with `--save-script`.
272+
- Session scripts are written to `<state-dir>/sessions/<session>-<timestamp>.ad` when recording is enabled with `--save-script`.
266273
- `--save-script` accepts an optional path: `--save-script ./workflows/my-flow.ad`.
267274
- For ambiguous bare values, use an explicit form: `--save-script=workflow.ad` or a path-like value such as `./workflow.ad`.
268275
- Deterministic replay is `.ad`-based; use `replay --update` (`-u`) to update selector drift and rewrite the replay file in place.
@@ -386,7 +393,7 @@ Clipboard:
386393

387394
## Debug
388395

389-
- **App logs (token-efficient):** Logging is off by default in normal flows. Enable it on demand when debugging. With an active session, run `logs path` to get path + state metadata (e.g. `~/.agent-device/sessions/<session>/app.log`). Run `logs start` to stream app output to that file; use `logs stop` to stop. Run `logs clear` to truncate `app.log` (and remove rotated `app.log.N` files) before a new repro window. Run `logs doctor` for tool/runtime checks and `logs mark "step"` to insert timeline markers. Grep the file when you need to inspect errors (e.g. `grep -n "Error\|Exception" <path>`) instead of pulling full logs into context. Supported on iOS simulator, iOS physical device, and Android.
396+
- **App logs (token-efficient):** Logging is off by default in normal flows. Enable it on demand when debugging. With an active session, run `logs path` to get path + state metadata (e.g. `<state-dir>/sessions/<session>/app.log`). Run `logs start` to stream app output to that file; use `logs stop` to stop. Run `logs clear` to truncate `app.log` (and remove rotated `app.log.N` files) before a new repro window. Run `logs doctor` for tool/runtime checks and `logs mark "step"` to insert timeline markers. Grep the file when you need to inspect errors (e.g. `grep -n "Error\|Exception" <path>`) instead of pulling full logs into context. Supported on iOS simulator, iOS physical device, and Android.
390397
- Use `logs clear --restart` when you want one command to stop an active stream, clear current logs, and immediately resume streaming.
391398
- `logs start` appends to `app.log` and rotates to `app.log.1` when the file exceeds 5 MB.
392399
- **Network dump (best-effort):** `network dump [limit] [summary|headers|body|all]` parses recent HTTP(s) lines from the same session app log file and returns method/url/status with optional headers/bodies. `network log ...` is an alias. Current limits: scans up to 4000 recent log lines, returns up to 200 entries, truncates payload/header fields at 2048 characters.
@@ -400,7 +407,7 @@ Clipboard:
400407
- The trace log includes snapshot logs and XCTest runner logs for the session.
401408
- Built-in retries cover transient runner connection failures and Android UI dumps.
402409
- For snapshot issues (missing elements), compare with `--raw` flag for unaltered output and scope with `-s "<label>"`.
403-
- If startup fails with stale metadata hints, remove stale `~/.agent-device/daemon.json` / `~/.agent-device/daemon.lock` and retry.
410+
- If startup fails with stale metadata hints, remove stale `<state-dir>/daemon.json` / `<state-dir>/daemon.lock` and retry (state dir defaults to `~/.agent-device` unless overridden).
404411

405412
Boot diagnostics:
406413
- Boot failures include normalized reason codes in `error.details.reason` (JSON mode) and verbose logs.
@@ -458,6 +465,15 @@ Environment selectors:
458465
- `IOS_DEVICE="iPhone 17 Pro"` or `IOS_UDID=<udid>`
459466
- `AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS=<ms>` to adjust iOS simulator boot timeout (default: `120000`, minimum: `5000`).
460467
- `AGENT_DEVICE_DAEMON_TIMEOUT_MS=<ms>` to override daemon request timeout (default `90000`). Increase for slow physical-device setup (for example `120000`).
468+
- `AGENT_DEVICE_STATE_DIR=<path>` override daemon state directory (metadata, logs, session artifacts).
469+
- `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).
470+
- `AGENT_DEVICE_DAEMON_TRANSPORT=auto|socket|http` client preference when connecting to daemon metadata.
471+
- `AGENT_DEVICE_HTTP_AUTH_HOOK=<module-path>` optional HTTP auth hook module path for JSON-RPC server mode.
472+
- `AGENT_DEVICE_HTTP_AUTH_EXPORT=<export-name>` optional export name from auth hook module (default: `default`).
473+
- `AGENT_DEVICE_MAX_SIMULATOR_LEASES=<n>` optional max concurrent simulator leases for HTTP lease allocation (default: unlimited).
474+
- `AGENT_DEVICE_LEASE_TTL_MS=<ms>` default lease TTL used by `agent_device.lease.allocate` and `agent_device.lease.heartbeat` (default: `60000`).
475+
- `AGENT_DEVICE_LEASE_MIN_TTL_MS=<ms>` minimum accepted lease TTL (default: `5000`).
476+
- `AGENT_DEVICE_LEASE_MAX_TTL_MS=<ms>` maximum accepted lease TTL (default: `600000`).
461477
- `AGENT_DEVICE_IOS_TEAM_ID=<team-id>` optional Team ID override for iOS device runner signing.
462478
- `AGENT_DEVICE_IOS_SIGNING_IDENTITY=<identity>` optional signing identity override.
463479
- `AGENT_DEVICE_IOS_PROVISIONING_PROFILE=<profile>` optional provisioning profile specifier for iOS device runner signing.

src/cli.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { readVersion } from './utils/version.ts';
55
import { pathToFileURL } from 'node:url';
66
import { sendToDaemon } from './daemon-client.ts';
77
import fs from 'node:fs';
8-
import os from 'node:os';
98
import path from 'node:path';
109
import type { BatchStep } from './core/dispatch.ts';
1110
import { parseBatchStepsJson } from './core/batch.ts';
1211
import { createRequestId, emitDiagnostic, flushDiagnosticsToSessionFile, getDiagnosticsMeta, withDiagnosticsScope } from './utils/diagnostics.ts';
12+
import { resolveDaemonPaths } from './daemon/config.ts';
1313

1414
type CliDeps = {
1515
sendToDaemon: typeof sendToDaemon;
@@ -97,8 +97,9 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS):
9797

9898
const { command, positionals, flags } = parsed;
9999
const daemonFlags = toDaemonFlags(flags);
100+
const daemonPaths = resolveDaemonPaths(flags.stateDir ?? process.env.AGENT_DEVICE_STATE_DIR);
100101
const sessionName = flags.session ?? process.env.AGENT_DEVICE_SESSION ?? 'default';
101-
const logTailStopper = flags.verbose && !flags.json ? startDaemonLogTail() : null;
102+
const logTailStopper = flags.verbose && !flags.json ? startDaemonLogTail(daemonPaths.logPath) : null;
102103
const sendDaemonRequest = async (payload: { command: string; positionals: string[]; flags?: Record<string, unknown> }) =>
103104
await deps.sendToDaemon({
104105
session: sessionName,
@@ -109,6 +110,10 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS):
109110
requestId,
110111
debug: Boolean(flags.verbose),
111112
cwd: process.cwd(),
113+
tenantId: flags.tenant,
114+
runId: flags.runId,
115+
leaseId: flags.leaseId,
116+
sessionIsolation: flags.sessionIsolation,
112117
},
113118
});
114119
try {
@@ -459,7 +464,7 @@ export async function runCli(argv: string[], deps: CliDeps = DEFAULT_CLI_DEPS):
459464
printHumanError(normalized, { showDetails: flags.verbose });
460465
if (flags.verbose) {
461466
try {
462-
const logPath = path.join(os.homedir(), '.agent-device', 'daemon.log');
467+
const logPath = daemonPaths.logPath;
463468
if (fs.existsSync(logPath)) {
464469
const content = fs.readFileSync(logPath, 'utf8');
465470
const lines = content.split('\n');
@@ -536,9 +541,8 @@ if (isDirectRun) {
536541
});
537542
}
538543

539-
function startDaemonLogTail(): (() => void) | null {
544+
function startDaemonLogTail(logPath: string): (() => void) | null {
540545
try {
541-
const logPath = path.join(os.homedir(), '.agent-device', 'daemon.log');
542546
let offset = 0;
543547
let stopped = false;
544548
const interval = setInterval(() => {

0 commit comments

Comments
 (0)