Skip to content

The agent-device front refuses requests that reach past the agent's device, and devices lists only its own #475

Closed

Description

@V3RON

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, else params.flags.leaseId). Example: agent A's
    snapshot names lease 9f3c…, whose entry holds the iPhone 17 Pro with
    UDID A1B2…. 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 for test a comma list of names and ids),
    and deviceKey (agent-device's key for a leased device). Example:
    agent-device screenshot --udid A1B2… sends flags.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
Loading
# 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 *"]
Loading

(* = changed, new = new file.)

The order of checks on a request. The first refusal answers.

  1. 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: ".
  2. Rule 1, the route (404).
  3. An agent opens and closes a device through simlock #472's parse of an /rpc body: its size limit, its JSON, the shape of a
    lease ID where one is present, and its refusal of an
    agent_device.command whose params.command is a lease command.
  4. Rules 2 to 9 below.
  5. 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:

  1. Route. /v1/agent-device followed by one of these, else 404 with
    refused: unknown route <method> <path>:

    Method Path
    GET /health
    POST /rpc
    POST /upload, /upload/preflight, /upload/finalize
    PUT /upload/direct/<id>
    GET /artifacts, /artifacts/, /artifacts/<path>
    GET /sessions/<session>/requests/<request id>/diagnostics

    This is the list @agent-device/proxy forwards (isSupportedDaemonRoute
    in packages/proxy/src/daemon-proxy.ts), read against the CLI's URLs:
    it joins health, rpc and upload/... 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/x and /upload/direct/a/b are refused.

  2. RPC method. On /rpc, method is 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.

  3. Where selectors are read, and their types.

    • agent_device.command: params.flags, params.input and, for the
      platform, params.runtime.platform. agent-device merges them with
      flags winning (src/daemon/macos-app-lease.ts:120), so the value a
      rule reads is flags, else input, else runtime; each rule checks
      every place a field appears, and a rewrite sets the field in flags
      and removes it from input. The device key is params.meta.deviceKey,
      else params.flags.deviceKey (agent-device's own order,
      packages/contracts/src/lease-scope.ts:95). For params.command
      batch, also each entry of params.flags.batchSteps (its input, its
      flags and its runtime).
    • agent_device.lease.allocate: the top-level params. An agent opens and closes a device through simlock #472 already
      refuses backend, retainOnClose, provider and aws* keys.
    • agent_device.install_from_source: params.platform.
    • Heartbeat and release carry no selector. agent-device's own scope check
      refuses a deviceKey there that differs from the lease's.
    • Types: platform, target, udid, serial, device and deviceKey
      must be strings when present; shardAll and shardSplit integers;
      params.flags, params.input and each batch step objects;
      batchSteps an array; a step's command a string. Anything else is
      refused: "refused: malformed ". iosSimulatorDeviceSet and
      androidDeviceAllowlist are not type-checked: rule 5 refuses them
      with any value.
    • target is 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 a target that does not match
      the session's device.
  4. Platform. platform, when present, is ios or android. 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".

  5. Device set and allowlist. iosSimulatorDeviceSet present, 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". androidDeviceAllowlist the same: "refused:
    simlock chooses the devices; run disconnect, then connect proxy without
    --android-device-allowlist". The CLI saves both flags at connect proxy
    and sends them on every command, so the message says how to clear them.

  6. Device selectors. Refusal: "refused: simlock picks the device".

    • Names are compared the way agent-device compares device names
      (normalizeDeviceName in packages/kernel/src/device.ts): letter case
      ignored, _ read as a space, runs of spaces read as one, ends trimmed.
    • udid and serial pass 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.
    • device on any command but test passes 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 as P8 accepted), so the
      front and simlock agree on what a model name means, the front removes
      device and sets the leased
      device's id as udid (iOS) or serial (Android). agent-device matches
      device by 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.
    • device on test is 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's deviceKey (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
      after close.
    • A request with no leased device refuses any udid, serial or
      device on a command other than test. An allocate's udid or
      serial is refused. An allocate's device is 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.
  7. Sharding. A test with shardAll or shardSplit and no device
    list is not refused. "No list" means device is absent, or a string
    that splits to no entries under rule 6 (such as "" or " , "). The
    front sets flags.device to the ids of the token's leased devices
    whose platform equals the request's platform and whose target equals
    its target, each compared only when the request sends it, joined with
    ,. agent-device would otherwise pick from everything its daemon sees,
    and it throws No 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.

  8. Install. install or reinstall without a meta.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 sends remote:<path>, or a path
    missing on the agent's machine, as a plain positional
    (prepareRemoteInstallPackage in src/remote/daemon-artifacts.ts). A
    batch step install or reinstall is always refused: a step carries
    no upload.

  9. Batch devices. A batch step devices is refused: "refused: run
    devices on its own".

  10. Commands agent-device runs without a lease. agent-device's
    lease-exempt commands (leaseAdmissionExempt in
    packages/command-registry/src/registry.ts) are lease_allocate,
    lease_heartbeat, lease_release, artifacts, session_list,
    devices, capabilities, doctor, events and
    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;
    devices is filtered below; artifacts, session_list, events and
    release_materialized_paths act only on the tenant's own sessions and
    files, and pass. capabilities and doctor resolve a device: with no
    udid, serial or device, 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), as udid (iOS) or serial (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
Loading

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 and udid/serial checks 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
    authenticate mode sends every request with sessionIsolation: tenant.
    A command with no session lease is then admitted only when it names a
    lease: assertRequestLeaseAdmission (src/daemon/request-admission.ts)
    calls normalizeLeaseAdmissionRequest
    (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
    plain close) pass without one. Of those, only capabilities and
    doctor can 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 a target, udid, serial or device that 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 kind path without 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 boot and
    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; the PUT /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.md rule 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.md rule 10: each rule is in one place. The platform and
    udid/serial checks 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.md rule 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/b and 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.command with a dash is forwarded
  • a command with --platform android is forwarded; with macos or apple, or runtime.platform harmonyos, it is refused; with --platform ios while the iOS driver is off it is refused with "refused: ios is not available on this simlock"; a batch step whose runtime.platform is web is refused
  • install_from_source with a URL source and platform ios is forwarded, as today; with platform web it is refused
  • a batch step with platform: web is refused and no step reaches agent-device
  • udid and serial of the agent's own leased device are forwarded; another device's are refused, in flags and in input alike
  • --device with 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 (p8 for a Pixel 8), it reaches agent-device as udid of the leased device on iOS and serial on Android, with no device; another model is refused
  • in a batch, a step with the leased model reaches agent-device with the leased id, a sharded test step 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 refused
  • test --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 with serial and one with device are 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 deviceKey is forwarded; with another lease's deviceKey, in meta or only in flags, it is refused; with no lease named, a stale deviceKey is forwarded
  • a batch step with another device's udid is refused and no step reaches agent-device
  • a udid that is a number is refused with "refused: malformed udid"; a shardAll that is a string with "refused: malformed shardAll"
  • --ios-simulator-device-set naming simlock's own device set is refused with the disconnect-and-reconnect message, on a command and on an allocate
  • --android-device-allowlist is refused with its disconnect-and-reconnect message, on a command and in a batch step
  • test --shard-all 2 with no --device reaches agent-device with device set 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 1 gets only the simulator's id, and for a token holding a phone and a TV, test --target tv --shard-all 1 gets only the TV's id, while a device whose reply named no target counts as mobile; for a token holding only the simulator, test --platform android --shard-all 1 is refused with "refused: no device leased; run open first"
  • test --shard-split 2 with device: " , " 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"
  • install with meta.uploadedArtifactId is forwarded; without it, with "" and with " " it is refused with "send the app from your machine as an upload"
  • reinstall without an upload is refused; batch steps install and " Install " are refused and no step reaches agent-device
  • devices before open answers an empty list while agent-device lists three devices; after open it answers only the leased device, without agent B's
  • devices for an agent holding two devices answers both, and after simlock release --all ended one lease (no heartbeat sent since), only the other
  • a devices error answer from agent-device passes unchanged; a success with no list at data.devices, a body over 1 MiB and a non-JSON body each answer an empty list; devices with meta.requestProgress is refused
  • capabilities with no selector reaches agent-device with the leased device's id; from a token holding no device it is refused; doctor the same
  • a route refusal answers 404 with refused: unknown route POST /rpcx
  • batch steps devices and " DEVICES " are refused
  • boot passes the rules and reaches agent-device, as before this PR
  • GET /sessions/<tenant>:s1/requests/r1/diagnostics reaches 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 that deviceKey reaches 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 in FakeAgentDevice's received requests.
  • A sharded test with no --device reaches FakeAgentDevice with the
    token's leased devices of its platform as its device.

Out of scope

Depends on

Approval

  • Approved for delivery

Written by an agent.

Activity

  1. added
    task:draftScope written; technical spec or its spec check missing.
    on Oct 12, 2026
  2. V3RON commented on Oct 12, 2026

    @V3RON
    ContributorAuthor

    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.

  3. V3RON commented on Oct 12, 2026

    @V3RON
    ContributorAuthor

    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.

  4. V3RON commented on Oct 12, 2026

    @V3RON
    ContributorAuthor

    Spec updated: closure-check findings fixed (maintainer asked to fix all).

    Written by an agent.

  5. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:draftScope written; technical spec or its spec check missing.
    on Oct 13, 2026
  6. github-actions commented on Oct 13, 2026

    @github-actions

    Now task:ready: approved, technical spec present, #474 closed or reviewed in this feature's stack.

  7. self-assigned this
    on Oct 13, 2026
  8. V3RON commented on Oct 13, 2026

    @V3RON
    ContributorAuthor

    Handoff

    Done: PR #507 (branch task/475, stacked on #506) implements every rule and the devices filter. 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:25 keeps deviceKey only 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:328 drops 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-character deviceKey is kept. The maintainer picks the device key's allowed characters; then one fix run on #506 and #507.

    Written by an agent.

  9. added
    task:blockedAgent stopped on a blocker in its handoff. Maintainer re-adds the label it came from.
    and removed
    task:readySpec checked. An agent may implement it once its Depends on allow.
    on Oct 13, 2026
  10. assigned and unassigned on Oct 13, 2026
  11. added
    task:readySpec checked. An agent may implement it once its Depends on allow.
    and removed
    task:blockedAgent stopped on a blocker in its handoff. Maintainer re-adds the label it came from.
    on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

task:readySpec checked. An agent may implement it once its Depends on allow.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions