Skip to content

Commit cb25b00

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-2022-adversarial-review-x9d9x3
# Conflicts: # CHANGELOG.md
2 parents 4b6e7c9 + 608bf7a commit cb25b00

54 files changed

Lines changed: 1641 additions & 288 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22

33
## Unreleased
44

5+
- Security (MCP/AI-SDK tool surface): the operator-owned endpoint and path inputs — `daemonBaseUrl`,
6+
the Metro `proxyBaseUrl`, `stateDir`, `cwd`, `iosSimulatorDeviceSet`, `iosXctestrunFile`,
7+
`iosXctestDerivedDataPath`, `iosXctestEnvDir` — follow the credential inputs off the
8+
model-writable tool surface: no longer advertised, refused as explicit input with guidance, and
9+
resolved from env/config only (a model-writable `daemonBaseUrl`/`proxyBaseUrl` would redirect the
10+
env-resolved token to an arbitrary server). Dropping these plus the credential fields shrinks
11+
`tools/list` by roughly half. CLI flags and the SDK client options are unchanged.
12+
- MCP tool descriptions now declare their enforced client timeout envelope (90s default, 180s
13+
install, 300s+ lease allocation, unbounded only for the streaming `test` runner), sourced from
14+
the descriptor registry's timeout policy so the declared number cannot drift from the enforced
15+
one.
16+
- Security (MCP/AI-SDK tool surface): the shared command-tool executor now enforces the advertised
17+
tool schema as an admission boundary — every raw `tools/call` argument must appear in the tool's
18+
advertised (`additionalProperties: false`) schema, or it is refused before config/env resolution.
19+
Hiding a key from `tools/list` alone was insufficient: the router forwards raw arguments verbatim
20+
and the MCP config resolver read `config`/`remoteConfig` as CLI flags, so a model-supplied config
21+
file could load `daemonBaseUrl`/`daemonAuthToken` and redirect the operator's token to an
22+
arbitrary endpoint. Deny-by-default closes that, the operator keys, and any unknown key at once;
23+
operator env/config defaults still resolve (they never arrive as tool input). Retired keys are
24+
still admitted so their migration guidance answers.
25+
- Security (MCP/AI-SDK tool surface): `daemonAuthToken` and the Metro `bearerToken` are no longer
26+
advertised as tool input properties, and an explicit value is refused with guidance instead of
27+
being forwarded. Credentials are operator-owned: set `AGENT_DEVICE_DAEMON_AUTH_TOKEN` (or
28+
`daemonAuthToken` in `~/.agent-device/config.json`) and `AGENT_DEVICE_METRO_BEARER_TOKEN` on the
29+
process serving the tools. The model both reads untrusted app UI text and picks tool arguments,
30+
so a model-writable credential parameter was a prompt-injection exfiltration path. CLI flags
31+
(`--daemon-auth-token`, `--bearer-token`) and env/config resolution are unchanged.
32+
- Release hygiene: after `npm publish`, `release:mark-dev` moves `main` to the next patch with a
33+
`-dev` prerelease marker so the version on `main` never equals a published version (registry
34+
scanners diff the tool surface per version string, and a moving surface under a released number
35+
reads as a republish). `release:prepare` refuses to publish while the `-dev` marker is in place.
536
- Windows `--platform web` works again. `agent-device web setup` no longer fails with
637
`npm not found in PATH`, and every web command — including `web doctor` — no longer fails with
738
`spawn EINVAL`. npm and the managed `agent-browser` backend are now both launched as

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ when a running daemon needs to pick up a new TypeScript build.
7272
`pnpm package:npm` is a release guard, not a routine development command. Use the specific commands
7373
above while iterating.
7474

75+
### The version on main never equals a published version
76+
77+
`release:publish` runs `release:mark-dev` right after `npm publish`, moving `package.json` (and the
78+
synchronized `server.json`) to the next patch with a `-dev` prerelease marker (for example
79+
`0.20.11-dev`). Commit that bump as part of the release. The invariant it protects: MCP registry
80+
scanners diff the repository's tool surface per version string, so a released number left on `main`
81+
while `main` keeps changing is indistinguishable from a republished ("rug-pull") version.
82+
`release:prepare` enforces the inverse direction and refuses to publish while the `-dev` marker is
83+
still in place — set the real release version first (for example `npm version patch`, which strips
84+
the prerelease marker), commit, then publish.
85+
7586
### Released-surface baselines roll forward on publish
7687

7788
Compatibility gates baseline against the last **released tag**, not against `main`, so publishing is

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent-device",
3-
"version": "0.20.10",
3+
"version": "0.20.11-dev",
44
"description": "Mobile app automation and verification for AI coding agents. CLI, MCP server, and typed Node.js API for iOS, Android, HarmonyOS, TV, web, macOS, and Linux.",
55
"mcpName": "io.github.callstack/agent-device",
66
"license": "MIT",
@@ -109,8 +109,9 @@
109109
"build:macos-helper:clean": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift package --package-path apple/macos-helper clean && pnpm build:macos-helper",
110110
"build:package": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android",
111111
"package:npm": "pnpm build:package && pnpm check:package",
112-
"release:prepare": "rm -rf .tmp/release && pnpm check:mcp-metadata && pnpm build:package && pnpm check:package -- --pack-destination .tmp/release",
113-
"release:publish": "pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz",
112+
"release:prepare": "node scripts/release-mark-dev.mjs --check-release-version && rm -rf .tmp/release && pnpm check:mcp-metadata && pnpm build:package && pnpm check:package -- --pack-destination .tmp/release",
113+
"release:publish": "pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz && pnpm release:mark-dev",
114+
"release:mark-dev": "node scripts/release-mark-dev.mjs",
114115
"ad": "node bin/agent-device.mjs",
115116
"bench:help-conformance": "node scripts/help-conformance-bench.mjs",
116117
"maestro:conformance": "node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/maestro-conformance/format-generated-json.test.mjs packages/maestro/test/conformance/verify.test.ts packages/maestro/test/conformance/differential/engine-process.test.ts packages/maestro/test/conformance/differential/report-output.test.ts packages/maestro/test/conformance/differential/run.test.ts packages/maestro/test/conformance/differential/invariants.test.ts",

packages/contracts/src/client-app.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ import type {
1515
} from './client-connection.ts';
1616
import type { AgentDeviceSessionDevice, StartupPerfSample } from './client-device-view.ts';
1717

18+
export type DeviceSelectionReason =
19+
| 'explicit-selector'
20+
| 'existing-session'
21+
| 'single-booted-local'
22+
| 'single-bootable-local'
23+
| 'single-app-installed-local'
24+
| 'preferred-local'
25+
| 'single-provider-device';
26+
27+
export type DeviceSelectionSource = 'session' | 'local' | 'provider';
28+
29+
/** Deterministic device-selection evidence shared by daemon responses and the published client. */
30+
export type DeviceSelectionMetadata = {
31+
reason: DeviceSelectionReason;
32+
source: DeviceSelectionSource;
33+
candidateCount: number;
34+
/** Whether this request booted a previously stopped local virtual target during open. */
35+
bootOccurred: boolean;
36+
};
37+
1838
export type AppInstallOptions = AgentDeviceRequestOverrides &
1939
AgentDeviceSelectionOptions & {
2040
app?: string;
@@ -73,6 +93,7 @@ export type AppOpenResult = {
7393
appId?: string;
7494
startup?: StartupPerfSample;
7595
runtime?: SessionRuntimeHints;
96+
selection?: DeviceSelectionMetadata;
7697
device?: AgentDeviceSessionDevice;
7798
/**
7899
* Initial interactive snapshot captured immediately after an open that

packages/contracts/src/facades/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export type {
1111
AppOpenResult,
1212
AppPushOptions,
1313
AppTriggerEventOptions,
14+
DeviceSelectionMetadata,
15+
DeviceSelectionReason,
16+
DeviceSelectionSource,
1417
MaterializationReleaseOptions,
1518
MaterializationReleaseResult,
1619
} from '../client-app.ts';

packages/kernel/src/device.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ export type DeviceSelector = {
6868
serial?: string;
6969
};
7070

71+
/** Device identity is narrower than platform/target filtering for precedence decisions. */
72+
export function hasExplicitDeviceIdentitySelector(
73+
selector: Pick<DeviceSelector, 'deviceName' | 'udid' | 'serial'>,
74+
): boolean {
75+
return [selector.deviceName, selector.udid, selector.serial].some(
76+
(value) => typeof value === 'string' && value.trim().length > 0,
77+
);
78+
}
79+
7180
type DeviceSelectionContext = {
7281
simulatorSetPath?: string;
7382
};

scripts/__tests__/test-file-size-ratchet.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const PINNED_TEST_FILE_LINES: Readonly<Record<string, number>> = Object.freeze({
5050
'src/__tests__/cli-config.test.ts': 1282,
5151
'src/daemon/handlers/__tests__/find.test.ts': 1202,
5252
'src/platforms/apple/core/__tests__/perf.test.ts': 1222,
53-
'src/mcp/__tests__/command-tools.test.ts': 1218,
53+
'src/mcp/__tests__/command-tools.test.ts': 1216,
5454
'src/daemon/handlers/__tests__/session-replay-divergence.test.ts': 1137,
5555
'src/platforms/apple/core/__tests__/apps.test.ts': 1210,
5656
'src/daemon/handlers/__tests__/session-replay-repair-transaction.test.ts': 1208,

scripts/release-mark-dev.mjs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { spawnSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import process from 'node:process';
5+
6+
// The version on main must never equal a published version: registry scanners
7+
// diff the repository's tool surface per version string, so a released number
8+
// left in place while main keeps moving reads as a rug-pull republish
9+
// (AS-012). `release:publish` runs this right after `npm publish` to move
10+
// main to the next patch with a `-dev` prerelease marker; `--check-release-version`
11+
// is the inverse guard in `release:prepare`, refusing to publish a `-dev`
12+
// version because the maintainer has not set the release version yet.
13+
const root = process.cwd();
14+
const checkReleaseVersion = process.argv.includes('--check-release-version');
15+
const packagePath = path.join(root, 'package.json');
16+
17+
const raw = fs.readFileSync(packagePath, 'utf8');
18+
const pkg = JSON.parse(raw);
19+
const version = pkg.version;
20+
21+
if (typeof version !== 'string' || version.length === 0) {
22+
fail('package.json must define version.');
23+
}
24+
25+
if (checkReleaseVersion) {
26+
if (version.includes('-')) {
27+
fail(
28+
`package.json version ${version} is a prerelease marker. Set the release version first ` +
29+
'(e.g. `npm version patch`), commit, then publish.',
30+
);
31+
}
32+
process.exit(0);
33+
}
34+
35+
if (version.includes('-')) {
36+
process.stdout.write(`Version ${version} already carries a prerelease marker; nothing to do.\n`);
37+
process.exit(0);
38+
}
39+
40+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
41+
if (!match) {
42+
fail(`Unsupported version format: ${version}`);
43+
}
44+
const nextVersion = `${match[1]}.${match[2]}.${Number(match[3]) + 1}-dev`;
45+
46+
const versionField = `"version": "${version}"`;
47+
if (raw.split(versionField).length !== 2) {
48+
fail(`Expected exactly one ${versionField} in package.json.`);
49+
}
50+
fs.writeFileSync(packagePath, raw.replace(versionField, `"version": "${nextVersion}"`));
51+
52+
const sync = spawnSync(process.execPath, [path.join(root, 'scripts', 'sync-mcp-metadata.mjs')], {
53+
stdio: 'inherit',
54+
});
55+
if (sync.status !== 0) {
56+
fail('sync-mcp-metadata failed after the version bump.');
57+
}
58+
59+
process.stdout.write(
60+
`Marked main as unreleased: ${version} -> ${nextVersion} (package.json + server.json).\n` +
61+
`Commit and push this so the version on main never equals the published ${version}.\n`,
62+
);
63+
64+
function fail(message) {
65+
process.stderr.write(`${message}\n`);
66+
process.exit(1);
67+
}

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
"url": "https://github.com/callstack/agent-device",
88
"source": "github"
99
},
10-
"version": "0.20.10",
10+
"version": "0.20.11-dev",
1111
"packages": [
1212
{
1313
"registryType": "npm",
1414
"identifier": "agent-device",
15-
"version": "0.20.10",
15+
"version": "0.20.11-dev",
1616
"transport": {
1717
"type": "stdio"
1818
}

src/__tests__/cli-help.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ test('help workflow prints the compact workflow card with a version header and s
8282
assert.match(result.stdout, /no CSS selectors/);
8383
assert.match(result.stdout, /help scripting/);
8484
assert.match(result.stdout, /help gestures/);
85+
assert.match(result.stdout, /Selection: explicit --device\/--udid\/--serial/);
86+
assert.match(result.stdout, /--platform\/--target only filter/);
87+
assert.match(result.stdout, /bounded retry selectors/);
8588
});
8689

8790
test('help workflow encourages chaining confident steps and requires the end state to be on screen', async () => {
@@ -141,7 +144,7 @@ test('help workflow advertises open --foreground and snapshot -i --actions', asy
141144
assert.equal(result.calls.length, 0);
142145
assert.match(
143146
result.stdout,
144-
/Known app: open <app> --foreground -> snapshot\. Bare form needs one running app on one iOS sim; capture failure keeps session open/,
147+
/open --foreground -> snapshot\. Selection: explicit --device\/--udid\/--serial/,
145148
);
146149
assert.match(
147150
result.stdout,

0 commit comments

Comments
 (0)