Repository navigation
The agent-device front refuses requests that reach past the agent's device, and devices lists only its own #475
Description
Part of #466.
Scope
After this PR, the front refuses every agent-device request that names a
device past the one simlock granted the agent: a platform other than iOS or
Android, a device selector naming any other device, a device set or Android
allowlist of the agent's own, a route or RPC method outside a fixed list, and
an install or reinstall of a path on the worker. A sharded test with no
--device runs on the devices this token holds. devices answers only
the devices this simlock token holds. Before this PR, all of these reach the
agent-device daemon, which picks any device it can see.
A command that names no lease and no device is not refused here:
agent-device itself refuses it, because the proxy runs every simlock token
as its own tenant (see Other code). The commands agent-device runs without a
lease are handled one by one (rule 10). A platform whose simlock driver is
off on this worker is refused.
Kept whole (about 33 Tests lines, several with more than one case): every
rule reads the same selectors and the same front map entry, and the
devices filter uses rule 6's notion of "the token's devices". The
maintainer chose not to split it.
In short
The front checks each request before it reaches the host. A refused request
never reaches agent-device and never touches a simlock lease. An accepted one
goes on as today. Two accepted requests are changed on their way: a sharded
test with no --device gets the token's leased devices added, and a
--device naming the leased device's model is sent as that device's id.
New terms:
- leased device (of a request): the device in the front map entry for
the lease the request names, when the request's principal holds that
lease. The lease is read with An agent opens and closes a device through simlock #472's shared lease ID reader
(params.meta.leaseId, elseparams.flags.leaseId). Example: agent A's
snapshotnames lease9f3c…, whose entry holds the iPhone 17 Pro with
UDIDA1B2…. A request that names no lease, or another principal's
lease, has no leased device. - the token's leased devices: the devices of every front map entry
whose lease the request's principal holds and that has not ended (Heartbeats keep the simlock lease alive #473's
marks). - device selector: a request field that picks a device:
udid,
serial,device(a name, or fortesta comma list of names and ids),
anddeviceKey(agent-device's key for a leased device). Example:
agent-device screenshot --udid A1B2…sendsflags.udid.
flowchart LR
CLI[agent-device CLI] --> T["token check, availability (#472)"]
T --> RT{"route (new)"}
RT -->|refused| N["404 (new)"]
RT --> P["body parse (#472)"]
P --> R{"request rules (new)"}
R -->|refused| E["error to the CLI (new)"]
R -->|accepted| L["lease translation (#472, #473)"]
L --> H[host] --> D[agent-device daemon]
D --> F["devices filter (new)"] --> CLI
# before (no rules: the agent-device daemon decides)
$ agent-device screenshot --udid <agent B's device>
# screenshot of agent B's device
$ agent-device install MyApp remote:/Users/ci/MyApp.app
# installs a file from the worker's disk
$ agent-device devices --platform ios
# every simulator simlock manages
# after
$ agent-device screenshot --udid <agent B's device>
error: refused: simlock picks the device
$ agent-device install MyApp remote:/Users/ci/MyApp.app
error: refused: send the app from your machine as an upload
$ agent-device open MyApp --platform web
error: refused: only iOS and Android
$ agent-device devices --platform ios # before open
(no devices)
$ agent-device open MyApp --platform ios --device "iPhone 17 Pro"
$ agent-device devices --platform ios
simlock-7Q2… booted <udid>
| Request | Today | After |
|---|---|---|
--platform ios / android |
passes | passes |
--platform web, macos, apple, vega, linux, harmonyos |
passes | refused: only iOS and Android |
--udid / --serial of the leased device |
that device | passes |
--udid / --serial of any other device |
that device | refused: simlock picks the device |
--udid / --serial / --device with no lease named (no leased device) |
that device | refused |
--device "iphone 17 pro", the leased device's model in any letter case |
a device named so | the leased device: sent as its udid / serial |
--device with the leased device's own name, or its id |
that device | passes |
--device naming another model or device |
that device | refused |
test --device a,b, every entry the id or name of one of the token's leased devices |
both | passes |
test --device <leased udid>,<another token's udid>, or an entry naming a model |
both | refused |
test --shard-all 2 or --shard-split 2 with no --device |
any booted devices | passes, with --device <the token's leased devices of that platform and target> added |
| the same, from a token that holds no such device | any booted devices | refused: no device leased; run open first |
batch with a step that has udid of another device |
that device | refused, no step runs |
--ios-simulator-device-set <any dir>, on any command (also when saved at connect proxy) |
that set | refused: simlock chooses the device set; run disconnect, then connect proxy without --ios-simulator-device-set |
--android-device-allowlist <any ids>, on any command (also when saved at connect proxy) |
that list | refused: simlock chooses the devices; run disconnect, then connect proxy without --android-device-allowlist |
install ./MyApp.app (an upload: meta.uploadedArtifactId set) |
installs | passes |
install MyApp remote:/path, or a path missing on the agent's machine |
reads the worker path | refused: send the app from your machine as an upload |
reinstall without an upload |
reads the worker path | refused |
batch with an install or reinstall step |
reads the worker path | refused |
batch with a devices step |
every device | refused: run devices on its own |
devices before open |
every device | empty list |
devices after open |
every device | only the devices this token holds |
boot, shutdown |
allowed | passes the rules; agent-device's policy file (#470) refuses it, as before this PR |
install-from-source with a URL source |
allowed | passes, as today |
a route outside the list, e.g. GET /v1/agent-device/admin/leases |
proxy answers 404 | refused, 404 |
If this goes wrong, a user would see an agent screenshot, install on or click
another agent's device, an app installed from a file on the worker, or
devices listing other agents' devices. Or, the other way, a legitimate
open --device "iPhone 17 Pro" or install ./MyApp.app refused.
Technical spec
Modules touched
flowchart LR
FH["front: request handler (#472) *"] --> RR["front: request rules (new)"]
FH --> DF["front: devices filter (new)"]
RR --> SR["shared: route and RPC method lists (new)"]
RR --> SS["shared: selector reading (new)"]
RR --> SC["shared: call classification, lease ID reader (#472)"]
RR --> FM["front: front map (#472, #473) *"]
DF --> FM
DF --> SU["shared: reply success rule (#472)"]
FH --> AM["front: allocate mapping (#472) *"]
FA["testing.ts: FakeAgentDevice *"]
(* = changed, new = new file.)
src/agent-device/shared/: the route list, the RPC method list, and the
reading of selector fields from RPC params. Suggested files:routes.ts
andselectors.ts. The method list sits beside An agent opens and closes a device through simlock #472's call
classification and uses its spelling rule (agent_device.and
agent-device.), so one place knows the spellings. If An agent opens and closes a device through simlock #472 already holds
a route list there, this task extends it rather than adding a second one.src/agent-device/front/: the rules (suggestedrequest-rules.ts), and
thedevicesfilter (suggesteddevices-filter.ts).- An agent opens and closes a device through simlock #472's allocate mapping checks the platform and refuses
udidandserial.
Both checks move into the rules, with An agent opens and closes a device through simlock #472's messages, and An agent opens and closes a device through simlock #472's tests for
them pass unchanged. The mapping reads the values the rules accepted, so
each rule lives in one place. Its no-platform, no-lease-ID and
unknown-model answers stay in An agent opens and closes a device through simlock #472, and so does its removal ofdevice,
deviceKeyandtargetfrom the allocate it forwards. - The front map entry. An agent opens and closes a device through simlock #472's entry holds the granted device's address,
which is the leased device's id. This task adds, when the entry is made:
the grant's platform andspec.model; and from agent-device's allocate
reply, thenameandtargetof thedeviceit names beside the lease
(targetread asmobilewhen absent, as agent-device reads it) and the
lease'sdeviceKey. A reply that names nodeviceleaves the name and
target unset (rule 6 then matches by id and model only, rule 7 reads the
target asmobile). A reply with nodeviceKey, or one that is not a
string of 1 to 128 characters, leaves it unset, and rule 6 then refuses any
deviceKeyon a request naming that lease. The reply names the device it picked from theudid
orserialAn agent opens and closes a device through simlock #472 sent (pickProxyLeaseDeviceand thedevicefield in
src/daemon/handlers/lease.ts, agent-device branch
feat/proxy-allocate-by-spec), from the same inventorydeviceslists. - An agent opens and closes a device through simlock #472's
HostLink.status()running state gainsplatforms, copied from
simlock runs the agent-device daemon #470's availability: the platforms whose simlock driver was built. src/agent-device/testing.ts:FakeAgentDevice(it already records every
request, An agent opens and closes a device through simlock #472) answersdeviceswith a list the test gives it, or with a
body the test gives it, and names the device in its allocate reply.
The order of checks on a request. The first refusal answers.
- An agent opens and closes a device through simlock #472's token check (401), then An agent opens and closes a device through simlock #472's availability check: while the edge
is on and agent-device is not available, every request, a route outside
the list included, answers "agent-device is not available on this
simlock: ". - Rule 1, the route (404).
- An agent opens and closes a device through simlock #472's parse of an
/rpcbody: its size limit, its JSON, the shape of a
lease ID where one is present, and its refusal of an
agent_device.commandwhoseparams.commandis a lease command. - Rules 2 to 9 below.
- An agent opens and closes a device through simlock #472's allocate mapping and the lease translation (An agent opens and closes a device through simlock #472, Heartbeats keep the simlock lease alive #473).
The rules:
-
Route.
/v1/agent-devicefollowed by one of these, else 404 with
refused: unknown route <method> <path>:Method Path GET /healthPOST /rpcPOST /upload,/upload/preflight,/upload/finalizePUT /upload/direct/<id>GET /artifacts,/artifacts/,/artifacts/<path>GET /sessions/<session>/requests/<request id>/diagnosticsThis is the list
@agent-device/proxyforwards (isSupportedDaemonRoute
inpackages/proxy/src/daemon-proxy.ts), read against the CLI's URLs:
it joinshealth,rpcandupload/...onto the daemon base URL
(src/daemon-client/daemon-client-transport.ts,
src/remote/upload-client.ts). Match by path segments, not by prefix.
<id>,<session>and<request id>are each exactly one non-empty
segment.<path>is one or more non-empty segments. Nothing else
matches:/rpcx,/rpc/xand/upload/direct/a/bare refused. -
RPC method. On
/rpc,methodis one of, else refused
("refused: unknown method "):
agent_device.command,agent_device.lease.allocate,
agent_device.lease.heartbeat,agent_device.lease.release,
agent_device.install_from_source,
agent_device.release_materialized_paths, each in both spellings. -
Where selectors are read, and their types.
agent_device.command:params.flags,params.inputand, for the
platform,params.runtime.platform. agent-device merges them with
flagswinning (src/daemon/macos-app-lease.ts:120), so the value a
rule reads isflags, elseinput, elseruntime; each rule checks
every place a field appears, and a rewrite sets the field inflags
and removes it frominput. The device key isparams.meta.deviceKey,
elseparams.flags.deviceKey(agent-device's own order,
packages/contracts/src/lease-scope.ts:95). Forparams.command
batch, also each entry ofparams.flags.batchSteps(itsinput, its
flagsand itsruntime).agent_device.lease.allocate: the top-level params. An agent opens and closes a device through simlock #472 already
refusesbackend,retainOnClose, provider andaws*keys.agent_device.install_from_source:params.platform.- Heartbeat and release carry no selector. agent-device's own scope check
refuses adeviceKeythere that differs from the lease's. - Types:
platform,target,udid,serial,deviceanddeviceKey
must be strings when present;shardAllandshardSplitintegers;
params.flags,params.inputand each batch step objects;
batchStepsan array; a step'scommanda string. Anything else is
refused: "refused: malformed ".iosSimulatorDeviceSetand
androidDeviceAllowlistare not type-checked: rule 5 refuses them
with any value. targetis read but is not a device selector: it names a kind of
device, never one device. Rule 7 uses it; inside a session,
agent-device's session selector refuses atargetthat does not match
the session's device.
-
Platform.
platform, when present, isiosorandroid. Anything
else is refused: "refused: only iOS and Android". A platform not in
HostLink.status().platforms(its simlock driver is off on this worker)
is refused: "refused: is not available on this simlock". -
Device set and allowlist.
iosSimulatorDeviceSetpresent, with any
value, on any command, allocate or batch step, is refused: "refused:
simlock chooses the device set; run disconnect, then connect proxy without
--ios-simulator-device-set".androidDeviceAllowlistthe same: "refused:
simlock chooses the devices; run disconnect, then connect proxy without
--android-device-allowlist". The CLI saves both flags atconnect proxy
and sends them on every command, so the message says how to clear them. -
Device selectors. Refusal: "refused: simlock picks the device".
- Names are compared the way agent-device compares device names
(normalizeDeviceNameinpackages/kernel/src/device.ts): letter case
ignored,_read as a space, runs of spaces read as one, ends trimmed. udidandserialpass only when equal to the leased device's id: the
udid or serial An agent opens and closes a device through simlock #472 sent agent-device in the allocate.deviceon any command buttestpasses when it equals the leased
device's id or matches its name. When it names the leased device's
model, matched the way simlock matches a model in a lease request
(findCatalogModel,src/core/catalog-match.ts, through core's index:
letter case ignored, catalog aliases such asP8accepted), so the
front and simlock agree on what a model name means, the front removes
deviceand sets the leased
device's id asudid(iOS) orserial(Android). agent-device matches
deviceby name only, and simlock's simulators are named
simlock-<id>(src/drivers/ios/index.ts:860), so a model sent on
would match nothing. Anything else is refused.deviceontestis split on,; each entry is trimmed and empty
ones dropped, as agent-device does (explicitShardDeviceSelectors,
packages/replay-port/src/daemon-port/session-test-shard-devices.ts).
Each entry must equal the id or match the name of one of the token's
leased devices: agent-device resolves a shard entry by id or name
only, never by model. Any other entry refuses it. This holds whether
or not the request names a lease.deviceKey, on a request that names a lease, passes only when equal
to the leased device'sdeviceKey(and is refused when the entry has
none). On a request that names no lease
it is not checked: agent-device reads it only as part of a lease's
scope (leaseScopeFromRequest), and the CLI keeps the last lease's key
afterclose.- A request with no leased device refuses any
udid,serialor
deviceon a command other thantest. An allocate'sudidor
serialis refused. An allocate'sdeviceis the model to ask for,
and An agent opens and closes a device through simlock #472's mapping answers it. - The front map entry answers until An agent opens and closes a device through simlock #472 removes it, one TTL after its
lease ended. Until then, a request naming the device of a lease simlock
ended passes this rule, and agent-device's "lease gone" answer reaches
the CLI. After that, the same request has no leased device and is
refused.
- Names are compared the way agent-device compares device names
-
Sharding. A
testwithshardAllorshardSplitand nodevice
list is not refused. "No list" meansdeviceis absent, or a string
that splits to no entries under rule 6 (such as""or" , "). The
front setsflags.deviceto the ids of the token's leased devices
whose platform equals the request'splatformand whose target equals
itstarget, each compared only when the request sends it, joined with
,. agent-device would otherwise pick from everything its daemon sees,
and it throwsNo shard device matched <id>for a listed device of
another platform or target (isShardDeviceCandidate, same file). When
no leased device is left, it is refused: "refused: no device leased;
run open first". A list the agent sent is checked by rule 6. The list
the front fills in is not checked again: it names only this token's
devices. -
Install.
installorreinstallwithout ameta.uploadedArtifactId
that is a string with something besides spaces is refused: "refused:
send the app from your machine as an upload". The proxy treats a
blank id as absent too (install-source-admission.ts). The CLI uploads
a local file and sets that id; it sendsremote:<path>, or a path
missing on the agent's machine, as a plain positional
(prepareRemoteInstallPackageinsrc/remote/daemon-artifacts.ts). A
batchstepinstallorreinstallis always refused: a step carries
no upload. -
Batch
devices. Abatchstepdevicesis refused: "refused: run
devices on its own". -
Commands agent-device runs without a lease. agent-device's
lease-exempt commands (leaseAdmissionExemptin
packages/command-registry/src/registry.ts) arelease_allocate,
lease_heartbeat,lease_release,artifacts,session_list,
devices,capabilities,doctor,eventsand
release_materialized_paths. The lease calls are An agent opens and closes a device through simlock #472's and Heartbeats keep the simlock lease alive #473's;
devicesis filtered below;artifacts,session_list,eventsand
release_materialized_pathsact only on the tenant's own sessions and
files, and pass.capabilitiesanddoctorresolve a device: with no
udid,serialordevice, the front sets the device of the lease
the request names, else the token's one leased device of the request's
platform (with no platform sent, the token's one leased device of any
platform), asudid(iOS) orserial(Android); otherwise it
refuses: "refused: name the device with --udid ; run devices to see
yours". A selector they carry goes through rule 6.
A batch step goes through rules 4 to 10 as a command does, with its own
selectors, and its leased device is the one of the lease the batch request
names. A step whose flags.leaseId names another lease is refused:
"refused: a batch step cannot name another lease". A step's command name is trimmed and lowercased before it is
compared, as agent-device does before it runs the step
(normalizeBatchCommandName, packages/command-registry/src/batch-policy.ts),
so " Install " is an install step. The first step refused refuses the
whole batch, and no step runs.
The devices filter. An agent_device.command with command: "devices"
that passed the rules goes on to agent-device. The daemon answers
result.data.devices, a list of entries with an id
(devicesInventoryResponse in
src/daemon/session-lifecycle/internal/inventory.ts). A devices request with meta.requestProgress set is refused: "refused:
devices does not stream through simlock", so the answer is always one JSON
body. The front reads that body up to 1 MiB. When #472's shared rule says the
answer is a success, the front keeps only the entries whose id is the id of
one of the token's leased devices. A JSON-RPC error envelope passes
unchanged. Anything else (a success without a list of objects at
result.data.devices, a body over 1 MiB, a body that is not one JSON
object) answers an empty list: the filter fails closed.
"The token's leased devices" (here and in rules 6, 7 and 10) are the
entries not ended. Before using them, the front checks those entries with
#472's per-principal lease.list (one call, bounded by 5 s) and marks any
lease simlock no longer lists ended through #473's who-ended function, so a
device simlock took back after release --all or expiry is never listed or
filled in. A lease.list that fails or times out answers the request with
COMMAND_FAILED and the message could not check your leases; try again.
Refusals on /rpc answer with #472's refusal reply, built by #472's shared
reply code: the JSON-RPC error envelope with the request's id,
error.code -32000, and error.data { code: "INVALID_ARGS", message },
the message being the refusal text. A route refusal answers HTTP 404 with
#472's other-route shape { ok: false, error: <text>, code: "INVALID_ARGS" }.
Assumption: --platform apple is refused. It is agent-device's alias for
any Apple device, macOS included; agents pass ios.
Assumption: a sharded test whose platform or target no leased device
matches gets the same refusal as a token holding no device.
One request through the hop, with the refusal:
sequenceDiagram
participant CLI as agent-device CLI
participant F as front
participant H as host
participant D as agent-device daemon
CLI->>F: POST /v1/agent-device/rpc agent_device.command {command, flags, meta.leaseId}
F->>F: token, availability, route, body parse (#472), then the request rules
alt refused
F-->>CLI: JSON-RPC error {code: -32000, data: {code: INVALID_ARGS, message: "refused: …"}}
else accepted
F->>H: forward (HostLink)
H->>D: through the proxy
D-->>H: result
alt command is devices
H-->>F: result
F->>F: keep only this principal's leased devices
end
F-->>CLI: result
end
Contract and event changes
None. Searched src/contract/: no operation, schema or error code changes;
the refusals are agent-device wire answers on /v1/agent-device/*. No
event is added or changed.
Other code on the same state
The state: the requests that reach agent-device, and the front map the rules
read.
- An agent opens and closes a device through simlock #472's token and availability checks run first, so a route outside
the list answers 401 without a token, and "not available" while
agent-device is not available. They win. - An agent opens and closes a device through simlock #472's body parse and allocate mapping. The parse runs before rules 2
to 9; the mapping after them, so a refused allocate never calls
lease.request. The platform andudid/serialchecks are in the rules
only. - An agent opens and closes a device through simlock #472's shared call classification, lease ID reader and reply success
rule. The rules and the filter call them; they keep no copy. - The front map (An agent opens and closes a device through simlock #472 writes and removes entries, Heartbeats keep the simlock lease alive #473 marks ended
ones). The rules and the filter only read it, apart from this task's new
fields written when the entry is made. The filter and rule 7 skip ended
entries; rule 6 reads an ended entry until An agent opens and closes a device through simlock #472 removes it. - agent-device's lease admission under tenant isolation. The proxy's
authenticatemode sends every request withsessionIsolation: tenant.
A command with no session lease is then admitted only when it names a
lease:assertRequestLeaseAdmission(src/daemon/request-admission.ts)
callsnormalizeLeaseAdmissionRequest
(src/daemon/lease-registry-scope.ts), which throws "tenant isolation
requires run id." or "... lease id." (agent-device branch
feat/proxy-authenticate, PR #3476). Only commands its registry marks
exempt (the lease calls,devices,session_list,capabilities,
doctor,events,artifacts,release_materialized_paths, and a
plainclose) pass without one. Of those, onlycapabilitiesand
doctorcan reach a device with no lease; rule 10 fills in or refuses
their device. Every other command with no lease is refused by agent-device
itself, and the front adds no rule for it. - agent-device's session selector (
src/daemon/session-selector.ts)
refuses atarget,udid,serialordevicethat does not match the
session's device. It runs after the front, as a second product's check. @agent-device/proxy's own checks, after the front: its route 404,
and its refusal of an install source of kindpathwithout an upload
(carriesUnbackedHostPathInstallSource,
packages/proxy/src/install-source-admission.ts). The front refuses
first, with simlock's text.- The agent-device daemon's policy file (simlock runs the agent-device daemon #470) refuses
bootand
shutdown, batch steps included (assertDaemonPolicyAdmitsRequest,
src/daemon/daemon-policy.ts). The rules do not repeat it. - agent-device refusing a request's iOS device set that differs from its
daemon-wide set (PR #3485, reason
ios_simulator_device_set_outside_daemon_set). The front refuses every
device set first, the daemon's own set included; the daemon's refusal
stays as a second product's check. agent-device has no such check for
the Android allowlist, so the front's is the only one. - Upload tickets. The proxy writes the caller's own credential into the
preflight answer; thePUT /upload/direct/<id>then passes the token
check and rule 1. Nothing here changes it. - simlock ending a simlock lease ends the agent-device session first #474's release scope record is filled on the host side from
agent-device's replies; the rules do not touch it.
Rules in play
safety.mdrule 10: every request field is a claim. Parse it, check its
type, compare whole values; never by prefix or substring. Unknown
routes, methods and selector shapes fail closed.architecture.mdrule 10: each rule is in one place. The platform and
udid/serialchecks leave An agent opens and closes a device through simlock #472's mapping; the lease ID, the call kind,
the spellings and reply success are An agent opens and closes a device through simlock #472's shared rules.- Rule 14: the rules and the filter stay inside
src/agent-device/; nothing
outside imports them. testing.mdrule 2: each refusal test first sends the accepted request
and shows it reaches agent-device, then changes one field.
Tests
Seam: the whole daemon in-process (startDaemon with a FakeDriver and a
FakeClock), the agent's calls over HTTP with fetch to
/v1/agent-device/*, FakeAgentDevice behind AgentDeviceUpstream through
StartDaemonOptions. Model: #472's src/agent-device/edge.test.ts and
src/daemon/main.test.ts (:1018-1090). Tests go in
src/agent-device/edge.test.ts. Each refusal test asserts the refusal text
and that FakeAgentDevice received nothing for it; each refused allocate
also asserts that simlock stored no lease request.
- each route in the list, with a token, reaches agent-device,
/artifacts/and an/artifacts/path of two segments included GET /admin/leases,POST /health,POST /rpcx,POST /rpc/x,PUT /upload/direct/a/band a diagnostics path with an extra segment are each refused 404 and reach nothing- a route outside the list answers 401 without a token, and "agent-device is not available on this simlock" while agent-device is unavailable
- an unknown RPC method is refused;
agent-device.commandwith a dash is forwarded - a command with
--platform androidis forwarded; withmacosorapple, orruntime.platformharmonyos, it is refused; with--platform ioswhile the iOS driver is off it is refused with "refused: ios is not available on this simlock"; a batch step whoseruntime.platformiswebis refused install_from_sourcewith a URL source and platformiosis forwarded, as today; with platformwebit is refused- a batch step with
platform: webis refused and no step reaches agent-device udidandserialof the agent's own leased device are forwarded; another device's are refused, inflagsand ininputalike--devicewith the leased device's id, and its name with_for a space, extra spaces and other letter case, are forwarded unchanged; with its model, or a catalog alias of it (p8for a Pixel 8), it reaches agent-device asudidof the leased device on iOS andserialon Android, with nodevice; another model is refused- in a batch, a step with the leased model reaches agent-device with the leased id, a sharded
teststep is filled as rule 7 says, and a step naming another lease is refused test --device <own udid>,<own name>is forwarded; for an agent holding two devices,test --device "<udid A>, ,<udid B>,"is forwarded; a list with an entry naming another token's device, or the leased device's model, is refusedtest --device <own udid>with no lease named is forwarded; naming the device of the token's own ended lease it is refused- with no lease named, a command with
udid, one withserialand one withdeviceare each refused - with no lease named and no selector, a command reaches agent-device unchanged
- a command naming its lease only in
flags.leaseId, with its own udid, is forwarded - a command naming agent B's lease with that lease's udid is refused; an agent holding two devices is refused lease B's udid on a command for lease A
- a command naming its lease with that lease's
deviceKeyis forwarded; with another lease'sdeviceKey, inmetaor only inflags, it is refused; with no lease named, a staledeviceKeyis forwarded - a batch step with another device's udid is refused and no step reaches agent-device
- a
udidthat is a number is refused with "refused: malformed udid"; ashardAllthat is a string with "refused: malformed shardAll" --ios-simulator-device-setnaming simlock's own device set is refused with the disconnect-and-reconnect message, on a command and on an allocate--android-device-allowlistis refused with its disconnect-and-reconnect message, on a command and in a batch steptest --shard-all 2with no--devicereaches agent-device withdeviceset to the token's leased devices, for an agent holding one device and one holding two, while another token's leased device is left out- for a token holding an iOS simulator and an Android emulator,
test --platform ios --shard-all 1gets only the simulator's id, and for a token holding a phone and a TV,test --target tv --shard-all 1gets only the TV's id, while a device whose reply named no target counts asmobile; for a token holding only the simulator,test --platform android --shard-all 1is refused with "refused: no device leased; run open first" test --shard-split 2withdevice: " , "is filled as if no list were sent; from a token that holds no device it is refused with "refused: no device leased; run open first"- after simlock ended the lease, a command with that lease's udid is forwarded and agent-device's answer reaches the CLI; one TTL later the same command is refused with "refused: simlock picks the device"
installwithmeta.uploadedArtifactIdis forwarded; without it, with""and with" "it is refused with "send the app from your machine as an upload"reinstallwithout an upload is refused; batch stepsinstalland" Install "are refused and no step reaches agent-devicedevicesbefore open answers an empty list while agent-device lists three devices; after open it answers only the leased device, without agent B'sdevicesfor an agent holding two devices answers both, and aftersimlock release --allended one lease (no heartbeat sent since), only the other- a
deviceserror answer from agent-device passes unchanged; a success with no list atdata.devices, a body over 1 MiB and a non-JSON body each answer an empty list;deviceswithmeta.requestProgressis refused capabilitieswith no selector reaches agent-device with the leased device's id; from a token holding no device it is refused;doctorthe same- a route refusal answers 404 with
refused: unknown route POST /rpcx - batch steps
devicesand" DEVICES "are refused bootpasses the rules and reaches agent-device, as before this PRGET /sessions/<tenant>:s1/requests/r1/diagnosticsreaches agent-device unchanged; the front does not rename the session
Assumption: the diagnostics route passes the session name through as the agent sent it; agent-device's proxy accepts only <tenant>:<session> names there, so the test uses tok_one:s1 and the front maps nothing (decided in delivery, confirmed by the maintainer 2026-10-13)
- a lease whose allocate reply names device key
ios:mobile:<id>keeps it: a command naming that lease with thatdeviceKeyreaches agent-device (device key rule decided by the maintainer on 2026-10-13)
Done when
- Every Tests line above passes with
pnpm exec vitest run --project unit <file>. - A request refused by each of rules 1 to 9 shows the refusal in the test
output and no matching entry inFakeAgentDevice's received requests. - A sharded
testwith no--devicereachesFakeAgentDevicewith the
token's leased devices of its platform as itsdevice.
Out of scope
- Selectors inside the script files
replayandtestread (.adand
Maestro flows): the front does not parse them. agent-device's session
selector and lease checks answer them. - Other flags that read or write worker files (
--out,--artifacts-dir,
--steps-file, Metro and XCTest paths): an accepted gap in Drive simlock devices with agent-device (worker) #466. - A selector flag agent-device adds later: it passes until agent-device
refuses it itself. - Refusing
bootandshutdown: simlock runs the agent-device daemon #470's policy file. - Mapping
--platform,--targetand--deviceto a simlock lease
request, removingdevice,deviceKeyandtargetfrom the forwarded
allocate, and the unknown-model answer: An agent opens and closes a device through simlock #472. - Proving it on real devices: End-to-end tests: agent-device through a simlock worker on real iOS and Android devices #477's refusal scenarios.
Depends on
Approval
- Approved for delivery
Written by an agent.
Activity
V3RON commentedon Oct 12, 2026
Spec updated: fixes from the spec check (check order, shared lease ID reader, id-or-name test lists, platform-filtered shard fill, deviceKey, input/runtime selectors, batch name normalising, model sent as id, new tests).
Written by an agent.
V3RON commentedon Oct 12, 2026
Spec updated: fixes from the second spec check (simlock's model match, lease-exempt commands, devices filter fails closed and checks leases first, field precedence, batch step lease, platforms whose driver is off).
Written by an agent.
V3RON commentedon Oct 12, 2026
Spec updated: closure-check findings fixed (maintainer asked to fix all).
Written by an agent.
github-actions commentedon Oct 13, 2026
Now
task:ready: approved, technical spec present, #474 closed or reviewed in this feature's stack.feat(agent-device): the front refuses requests that reach past the ag…
feat(agent-device): document the front's refusals; test the lease che…
refactor(agent-device): split the selector rule and the rules entry t…
refactor(agent-device): split the rules entry further to keep complex…
test(agent-device): the diagnostics route test uses the session name …
test(agent-device): a batch install step is refused despite an upload…
fix(agent-device): one address-field helper, drop a duplicate deviceK…
V3RON commentedon Oct 13, 2026
Handoff
Done: PR #507 (branch
task/475, stacked on #506) implements every rule and thedevicesfilter. Unit tests are green at 8278aeb. Review round 1 had 9 fixes and round 2 had 1, all applied. One decision is pending you: diagnostics session names pass through unchanged (see the PR comment).Not done: the fix check is open, steps 4–5 (Review section, notes comment) are not done, and the PR is still a draft.
Findings (fix check, open, src):
src/agent-device/shared/release-scope.ts:25keepsdeviceKeyonly if it matches[A-Za-z0-9._-]{1,128}. agent-device's real keys (ios:mobile:<id>) are dropped, so release-all sends no device key and agent-device refuses the release.src/agent-device/front/allocate.ts:328drops the same keys, so rule 6 refuses every command that names the lease.Blocked on: the specs disagree. #474 says each release scope field is 1–128 characters of letters, digits,
.,_,-. #475 says any 1–128-characterdeviceKeyis kept. The maintainer picks the device key's allowed characters; then one fix run on #506 and #507.Written by an agent.