Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
74ed540
refactor(plugins): colocate provider credential fingerprint
thymikee Oct 30, 2026
7b2f11a
refactor(daemon-client): extract src/daemon-client into @agent-device…
thymikee Oct 30, 2026
1d58571
refactor(daemon-client): name package tests for their modules and cut…
thymikee Oct 30, 2026
ee58ea2
test(daemon-client): bind only the registered-daemon fixtures the pac…
thymikee Oct 30, 2026
df6fd3f
refactor(daemon-client): send to a recorded daemon through the client…
thymikee Oct 30, 2026
1a561d6
refactor(daemon-client): publish the production send primitive and bu…
thymikee Oct 30, 2026
1f40ba5
fix(daemon-client): verify recorded daemon ownership and host propaga…
thymikee Oct 30, 2026
9d60eec
test(daemon-client): keep fixture startup errors when teardown fails;…
thymikee Oct 30, 2026
b012cde
style(plugins): group the manifest export after imports
thymikee Oct 30, 2026
c2b0a2d
fix(daemon-client): clarify tooling errors and client API contracts
thymikee Oct 30, 2026
b36fe00
test(daemon-client): retain one entry-target check per package after …
thymikee Oct 30, 2026
cf7f4f7
test(daemon-client): isolate build output and model recorder shutdown
thymikee Oct 30, 2026
505d5f9
test(daemon-client): pin whitespace provider fingerprint parity
thymikee Oct 31, 2026
f4d6ef2
fix(daemon-client): share registered daemon admission with tooling
thymikee Oct 31, 2026
b020cca
refactor(daemon-client): keep admission below lifecycle machinery
thymikee Oct 31, 2026
0441c18
fix(daemon-client): preserve refusals and benchmark admission diagnos…
thymikee Oct 31, 2026
aac253f
fix(benchmark): retain cell admission failures when cleanup refuses
thymikee Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/adr/0016-active-session-script-publication.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ The daemon never computes it by a re-parse of the script for `close`.
This field lets the real CLI/IPC client (not only the in-process handler) actually honor "the session stays active".
Before this field, the response had no explicit signal.
The one-shot `replay`/`test` teardown of the client had no way to distinguish a still-active handoff from a finished one.
It tore the owning daemon down regardless (`src/daemon-client/daemon-client-lifecycle.ts`).
It tore the owning daemon down regardless (`packages/daemon-client/src/daemon-client-lifecycle.ts`).
`sessionActive` is `true` for every close-less run, including a `--from` resume.
It is `false` once the terminal `close` of the script, or the deferred equivalent of a repair-armed run, has executed.

Expand Down
4 changes: 2 additions & 2 deletions docs/adr/0033-collocation-decision-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ The `client-side` logical-module policy prohibits every edge into `packages/daem
- CLI.
- Typed client.
- Remote transports.
- `src/daemon-client/`.
- `packages/daemon-client/src/`.

R78 also prohibits access through any root module reachable by the CLI process.
#3288's `daemon-contracts` zone declared ownership below both daemon and client.
Expand All @@ -163,7 +163,7 @@ They are not independent proof that the placement is optimal.
| `src/platform-runtime-daemon-lifecycle.ts` | retain for this batch | The same path-keyed R13/ADR 0022 seam applies. Relocation is possible only with coordinated changes to those declarations. |
| `src/platform-runtime-device-boot.ts` | retain for this batch | The same path-keyed R13/ADR 0022 seam applies. |
| `src/platform-runtime-resource-cleanup.ts` | retain for this batch | The same path-keyed R13/ADR 0022 seam applies. |
| `src/provider-credential-fingerprint.ts` | keep (type extracted) | Like `daemon-policy-file.ts`, this shared daemon/client contract follows #2559. #3288 declares its zone beside the provider composition that reads the credentials this file fingerprints. Daemon extraction PR 6 moved only the `DaemonProviderCredentials` declaration to `packages/daemon-contracts` at `./provider-credentials`. This is the same shared-below-both landing as the policy file. The implementation remains here because it reads the installed-plugin manifest. |
| `src/provider-credential-fingerprint.ts` | keep (type extracted) | Like `daemon-policy-file.ts`, this shared daemon/client contract follows #2559. #3288 declares its zone beside the provider composition that reads the credentials this file fingerprints. Daemon extraction PR 6 moved only the `DaemonProviderCredentials` declaration to `packages/daemon-contracts` at `./provider-credentials`. This is the same shared-below-both landing as the policy file. Credential fingerprinting resides beside the installed-plugin manifest lookup in `packages/plugins/src/credential-fingerprint.ts`. The root retains daemon credential composition. |
| `src/provider-limrun-runtime.ts` | keep | The proposed provider-limrun destination has no seam. Its constructor builds the root dependency factory `src/sdk/limrun-runtime-dependencies.ts`, which is the ADR 0019 composition seam. This loads the root adb-host binder, core Android interactor, and platform-runtime app-state helpers, all prohibited package imports. Moving the class also moves that composition. That is a design change, not a move. |
| `src/request-progress-protocol.ts` | moved → `packages/daemon-contracts/src/request-progress-protocol.ts` | This follows #2559's shared-contract decision. The client reads it statically through `daemon-client-progress.ts`; the server reads it through `packages/daemon/src/server/`. Only shared placement and wire-compat ledgers constrain it. #3288 declared `daemon-contracts`; extraction made it physical and moved wire-ledger keys with it. Contracts was rejected because R18 excludes envelope validation mechanics. |
| `src/runtime-command-surface.ts` | moved → `packages/command-runtime/src/index.ts` (after [#3299](https://github.com/callstack/agent-device/pull/3299) into `src/command-runtime/`) | The binding now owns the `@agent-device/command-runtime` entry. It retains the three-family daemon load shape. |
Expand Down
2 changes: 1 addition & 1 deletion docs/agents/code-ownership.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ For SDK entries `android-adb`, `artifacts`, `limrun`, `metro`, and `remote-confi
For command-host readers, use `withSdkHost`.
Keep provider callbacks on their supplied host.
Request cancellation and progress belong in `@agent-device/host-kit/request`.
Per-call abort guards belong beside transports in `src/daemon-client/daemon-client-transport.ts`.
Per-call abort guards belong beside transports in `packages/daemon-client/src/daemon-client-transport.ts`.
Contracts shared across layers belong in `packages/contracts/src`.
Schema composition belongs in `packages/command-surface/src/schema`.
A command facet declares no output formatter. Register the formatter in `src/commands/cli-output.ts`.
Expand Down
4 changes: 2 additions & 2 deletions docs/evidence/ios-keyboard-assistant-press/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ import { pathToFileURL } from 'node:url';
const root = process.cwd();
const load = path => import(pathToFileURL(`${root}/${path}`).href);
const { createAgentDeviceClient } = await load('dist/src/index.js');
const { readDaemonInfo } = await load('src/daemon-client/daemon-client-metadata.ts');
const { sendRequest } = await load('src/daemon-client/daemon-client-transport.ts');
const { readDaemonInfo } = await load('packages/daemon-client/src/daemon-client-metadata.ts');
const { sendRequest } = await load('packages/daemon-client/src/daemon-client-transport.ts');
const { resolveDaemonPaths } = await load('packages/daemon-contracts/src/daemon-resolution.ts');
const { createSnapshotBackendConformanceTransport } = await load('test/integration/ios-simulator-e2e/snapshot-backend-conformance.ts');
const { STATE_DIR: stateDir, UDID: udid } = process.env;
Expand Down
1 change: 1 addition & 0 deletions examples/sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
{ "path": "../../packages/command-surface" },
{ "path": "../../packages/contracts" },
{ "path": "../../packages/daemon" },
{ "path": "../../packages/daemon-client" },
{ "path": "../../packages/daemon-contracts" },
{ "path": "../../packages/device-selection" },
{ "path": "../../packages/host-kit" },
Expand Down
4 changes: 2 additions & 2 deletions fallow-baselines/health.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
},
"runtime_coverage_findings": [],
"target_keys": [
"src/daemon-client/daemon-client.ts:high impact",
"packages/daemon-client/src/index.ts:high impact",
"src/cli/parser/args.ts:high impact",
"packages/daemon/src/core/lease-context.ts:high impact",
"packages/replay-port/src/daemon-port/session-replay-heal.ts:high impact",
Expand Down Expand Up @@ -117,7 +117,7 @@
"packages/platform-android/src/app-parsers.ts:high impact",
"packages/platform-android/src/ui-hierarchy.ts:high impact",
"packages/platform-android/src/sdk.ts:high impact",
"src/daemon-client/daemon-client-lifecycle.ts:complexity",
"packages/daemon-client/src/daemon-client-lifecycle.ts:complexity",
"packages/react-native/src/companion/client-companion-tunnel-worker.ts:complexity"
]
}
6 changes: 3 additions & 3 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ const LEGACY_FILE_LINE_CAPS = {
'src/__tests__/cli-network.test.ts': 1092,
'src/__tests__/remote-connection.test.ts': 2324,
'packages/command-runtime/src/commands/interaction/runtime/settle.test.ts': 2094,
'src/daemon-client/__tests__/daemon-client-lifecycle.test.ts': 1169,
'src/daemon-client/__tests__/daemon-client.test.ts': 1340,
'packages/daemon-client/src/__tests__/daemon-client-lifecycle.test.ts': 1169,
'packages/daemon-client/src/__tests__/index.test.ts': 1340,
'src/mcp/__tests__/command-tools.test.ts': 1029,
'test/integration/provider-scenarios/android-lifecycle.test.ts': 1227,
'packages/host-kit/src/internal/exec.ts': 1032,
Expand Down Expand Up @@ -174,7 +174,7 @@ export default defineConfig({
},
{
files: [
'src/daemon-client/daemon-client-rpc.ts',
'packages/daemon-client/src/daemon-client-rpc.ts',
'packages/daemon/src/server/downloadable-artifact-http.ts',
'packages/remote/src/remote-request-diagnostics.ts',
'packages/remote/src/upload-stream.ts',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@
"@agent-device/command-surface": "workspace:*",
"@agent-device/contracts": "workspace:*",
"@agent-device/daemon": "workspace:*",
"@agent-device/daemon-client": "workspace:*",
"@agent-device/daemon-contracts": "workspace:*",
"@agent-device/device-selection": "workspace:*",
"@agent-device/host-kit": "workspace:*",
Expand Down
68 changes: 68 additions & 0 deletions packages/contracts/src/lease-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppError } from '@agent-device/kernel/errors';
import { stripUndefined } from '@agent-device/kernel/record';
import type { CloudProviderProfileFields } from './remote-config-fields.ts';
import type { CommandFlags } from './command-flags.ts';
import type { DaemonWireRequest } from './daemon-wire-request.ts';
import type { EndedLease, LeaseDevice, LeaseDeviceSpec } from './client-lease.ts';

const PROXY_LEASE_PROVIDER = 'proxy';
Expand Down Expand Up @@ -536,3 +537,70 @@ function readFlagString(
const value = flags?.[key];
return typeof value === 'string' ? value : undefined;
}

/**
* The JSON-RPC envelope a client puts on the daemon's `/rpc` endpoint for one request: the command
* method for an ordinary request, and the lease method with its lease params for a lease command.
* The caller supplies the request id, so the envelope is the same for every client.
* Provider values are forwarded verbatim; lease-provider normalization belongs to the daemon.
*/
export function buildDaemonRpcRequest(
req: DaemonWireRequest,
options: { id: string; includeTokenParam: boolean },
): {
jsonrpc: '2.0';
id: string;
method: string;
params: DaemonWireRequest | Record<string, unknown>;
} {
const { id } = options;
if (!isLeaseRpcCommand(req.command)) {
return {
jsonrpc: '2.0',
id,
method: 'agent_device.command',
params: req,
};
}
return {
jsonrpc: '2.0',
Comment thread
thymikee marked this conversation as resolved.
Resolved
id,
method: leaseRpcMethodForCommand(req.command),
params: {
...buildLeaseRpcParams(req, req.command, options),
...(req.command === 'lease_allocate' ? readLeaseAllocateProviderFlags(req.flags) : {}),
...(req.command === 'lease_allocate' && req.meta?.providerCredentialFingerprint
? { providerCredentialFingerprint: req.meta.providerCredentialFingerprint }
: {}),
},
};
}

function isLeaseRpcCommand(command: string): command is LeaseRpcCommand {
return (
command === 'lease_allocate' || command === 'lease_heartbeat' || command === 'lease_release'
);
}

function leaseRpcMethodForCommand(command: LeaseRpcCommand): string {
switch (command) {
case 'lease_allocate':
return 'agent_device.lease.allocate';
case 'lease_heartbeat':
return 'agent_device.lease.heartbeat';
case 'lease_release':
return 'agent_device.lease.release';
}
}

function buildLeaseRpcParams(
req: DaemonWireRequest,
command: LeaseRpcCommand,
options: { includeTokenParam: boolean },
): Record<string, unknown> {
return leaseScopeToLeaseRpcParams(leaseScopeFromRequest(req), command, {
includeTokenParam: options.includeTokenParam,
token: req.token,
session: req.session,
});
}
27 changes: 27 additions & 0 deletions packages/daemon-client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@agent-device/daemon-client",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "The client side of the daemon wire: transport and JSON-RPC framing, request cancellation and progress, local daemon launch, ensure, attach and stop, and remote daemon health. Internal workspace package bundled into the published agent-device artifact.",
"dependencies": {
"@agent-device/ad-replay": "workspace:*",
"@agent-device/command-registry": "workspace:*",
"@agent-device/contracts": "workspace:*",
"@agent-device/daemon-contracts": "workspace:*",
"@agent-device/host-kit": "workspace:*",
"@agent-device/kernel": "workspace:*",
"@agent-device/plugins": "workspace:*",
"@agent-device/remote": "workspace:*"
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"devDependencies": {
"@agent-device/proxy": "workspace:*",
"@agent-device/test-kit": "workspace:*"
}
}
11 changes: 11 additions & 0 deletions packages/daemon-client/src/__tests__/code-signature-fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { computeDaemonCodeSignature } from '@agent-device/host-kit/code-signature';
import { resolveDaemonLaunchSpec } from '../daemon-launch-spec.ts';

/**
* The code signature a daemon started from this checkout records, read through the same launch
* spec the client uses to launch and sign its daemon, so a fixture daemon is reused.
*/
export function currentDaemonCodeSignature(): string {
const spec = resolveDaemonLaunchSpec();
return computeDaemonCodeSignature(spec.useSrc ? spec.srcPath : spec.distPath, spec.root);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
isProcessAlive,
readHostProcessIdentityObservations,
} from '@agent-device/host-kit/process';
import { finishRegisteredDaemonFixtures } from '../../__tests__/test-utils/registered-daemon-fixture.ts';
import { finishRegisteredDaemonFixtures } from './registered-daemon-fixture.ts';
import {
draining,
launchesFreshDaemonAt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
finishRegisteredDaemonFixtures,
spawnRegisteredDaemonFixture,
waitForRegisteredDaemonFixture,
} from '../../__tests__/test-utils/registered-daemon-fixture.ts';
import { currentDaemonCodeSignature } from '../../__tests__/test-utils/daemon-http-fixture.ts';
} from './registered-daemon-fixture.ts';
import { currentDaemonCodeSignature } from './code-signature-fixture.ts';
import {
busy,
createDaemonLeaver,
Expand All @@ -37,7 +37,7 @@ vi.mock('@agent-device/host-kit/retry', async (importOriginal) => ({
import { runCmdDetachedMonitored } from '@agent-device/host-kit/command';
import { sleep } from '@agent-device/host-kit/retry';
import * as transport from '../daemon-client-transport.ts';
import { sendToDaemon } from '../daemon-client.ts';
import { sendToDaemon } from '../index.ts';
import { ensureDaemon } from '../daemon-client-ensure.ts';

// The explicit lifecycle: what an attach client may never do, and what `ensureDaemon` may do only
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import assert from 'node:assert/strict';
import { afterEach, test, vi } from 'vitest';
import { closeLoopbackServer, supportsLoopbackBind } from '@agent-device/test-kit';
import {
closeLoopbackServer,
createTestHostEnvironment,
supportsLoopbackBind,
} from '@agent-device/test-kit';
import { AppError } from '@agent-device/kernel/errors';
import { isProcessAlive } from '@agent-device/host-kit/process';
import { finishRegisteredDaemonFixtures } from '../../__tests__/test-utils/registered-daemon-fixture.ts';
import { finishRegisteredDaemonFixtures } from './registered-daemon-fixture.ts';
import {
busy,
createDaemonLeaver,
Expand All @@ -22,7 +26,7 @@ vi.mock('@agent-device/host-kit/command', async (importOriginal) => ({
import { runCmdDetachedMonitored } from '@agent-device/host-kit/command';
import { withEnsuredDaemon } from '../daemon-client-ensuring.ts';
import { daemonNotRunningError } from '../daemon-lifecycle-errors.ts';
import { createEnsuredDaemonTransport, type sendToDaemon } from '../daemon-client.ts';
import { createEnsuredDaemonTransport, type sendToDaemon } from '../index.ts';
import * as ensureModule from '../daemon-client-ensure.ts';

// The CLI and MCP reach the daemon through `withEnsuredDaemon`. The transport underneath is a
Expand Down Expand Up @@ -277,7 +281,10 @@ test('concurrent first requests share one ensure through the CLI and MCP transpo
const { stateDir, paths } = stateDirAndPaths();
const fresh = await startScriptedDaemon(okResult({ via: 'fresh' }));
launchesFreshDaemonAt(spawn, paths, fresh.port);
const ensuring = createEnsuredDaemonTransport();
const firstHost = createTestHostEnvironment();
const secondHost = createTestHostEnvironment();
const ensuring = createEnsuredDaemonTransport({ host: firstHost });
const secondEnsuring = createEnsuredDaemonTransport({ host: secondHost });
const flags = { stateDir, daemonTransport: 'http' };
const ensure = vi.spyOn(ensureModule, 'ensureDaemon');

Expand All @@ -292,7 +299,14 @@ test('concurrent first requests share one ensure through the CLI and MCP transpo
{ ok: true, data: { via: 'fresh' } },
]);
assert.equal(ensure.mock.calls.length, 1, 'one ensure for both requests');
assert.equal(spawn.mock.calls.length, 1, 'one daemon start for both requests');
assert.equal(ensure.mock.calls[0]?.[1], firstHost);
assert.deepEqual(await secondEnsuring(request('devices', flags)), {
ok: true,
data: { via: 'fresh' },
});
assert.equal(ensure.mock.calls.length, 2, 'a separate transport ensures again');
assert.equal(ensure.mock.calls[1]?.[1], secondHost);
assert.equal(spawn.mock.calls.length, 1, 'both transports reuse one daemon');
} finally {
await closeLoopbackServer(fresh.server);
}
Expand Down
Loading
Loading