Repository navigation
feat: let agents drive hosted macOS apps through agent-device macos-app leases - #2516
Conversation
janicduplessis
left a comment
There was a problem hiding this comment.
Fresh review of the diff against origin/main, the issue, the tests and guidance, and the agent-device branch (macos-app-lease.ts, host-lease-http.ts, daemon-proxy.ts, http-server.ts). The wire assumptions match: /admin/leases/<id> PUT and DELETE with the daemon token from daemon.json, the PUT body fields, /health upstream.leaseBackends, the policy keys (leases.require, commands.allow), AGENT_DEVICE_DAEMON_POLICY, AGENT_DEVICE_MACOS_APP_BACKEND, the meta field names and the remote-config keys. CI is green. Findings below; I read the code and did not run the agent-device daemon.
1. Blocking: the pinned RPC still passes host paths and runtime hints to the daemon (packages/server/src/agent-device-driver.ts:520-541)
pinLease overwrites the owner fields in meta but spreads the rest of the client's params and meta through unchanged. It is a list of fields to override, not a list of fields to allow. agent-device's macos-app admission (ad/src/daemon/macos-app-lease.ts:81-150) checks only the command name, surface, platform, screenshotFullscreen, and the open target and flags.launchUrl. It does not check any host-filesystem or URL field. Reading the daemon code, a client holding a grant can send:
screenshotwithpositionals[0]orflags.outset to an absolute path.readScreenshotRequest(ad/src/daemon/screenshot-runtime.ts:313-329) expands it withmeta.cwdand writes the capture there. That is a write to any file the hosting user can write, including via ascreenshotstep insidebatch.diff screenshotwith--baselineand--outpaths (diffis on the allow list).open <bundleId>withruntime.launchUrl(the RPCruntimeparam, notflags.launchUrl).assertOpensLeasedAppchecks onlyfields.launchUrl, andsession-open-execution.ts:240passesruntimeHints.launchUrlon to a follow-up URL open.flags.launchConsole,meta.cwd,meta.developerDir,meta.lockPolicy,meta.lockPlatform,meta.installSource,meta.uploadedArtifactId,meta.clientArtifactPaths,meta.retainMaterializedPaths.
The issue says no unscoped access is handed out and the lease confines screenshots to the window. A client can still reach the host filesystem and open URLs on the host, so this contradicts the PR's stated guarantee. The pins every command... test only covers the owner fields, so nothing would catch it.
Suggested fix, in two places:
- Stim: build
metafrom an allowlist (requestId,debug,includeCost,responseLevel,sessionExplicit) plus the owner fields, dropparams.runtime, and for the command RPC reject path-bearing arguments (screenshotordiffpositionals andflags.out/baseline/launchConsole). Either recurse intoflags.batchStepsor removebatchfrom the policy allow list until agent-device covers it. - agent-device (the actual enforcement point): have
assertMacOsAppLeaseAdmitsRequestrefuse output and baseline paths andreq.runtime.launchUrl. Worth raising on callstack/agent-device#3229. - Add a test with each of these fields in a client request.
2. Non-blocking: issue() puts the new lease before revoking the old one (agent-device-driver.ts:416-417)
putHostLease refuses a second lease for the same tenant, run and device key (DEVICE_IN_USE, ad/src/daemon/lease-registry.ts:101-120). HostedAgentHost.appRunning drops an existing entry before calling issue(), so the normal path is fine. A direct second issue() for the same session fails on the PUT. If the later revoke throws (daemon down), issue() rejects with the new lease already allocated and no renew timer, so it lingers up to 10 minutes. Revoke first, or make issue() idempotent per session.
3. Non-blocking: lease renewal failure is only logged (agent-device-driver.ts:418-424)
If a renewal PUT fails (daemon restarting, or the Mac slept past the 10-minute window so the lease expired), the grant stays live in HostedAgentHost while every client call fails with LEASE_NOT_FOUND until the next 2-minute tick, and forever if PUT keeps failing. The PUT on an expired id recreates it, so it self-heals, but a persistent failure should revoke the grant and report none with a notice. A test with FAKE_ADMIN=refuse after a successful issue would pin the behavior.
4. Non-blocking, unverified: daemon sessions across an app relaunch
The tenant is stim.<session>, which is stable for the Stim session. When the hosted app restarts (new pid, new lease id, same tenant), a daemon session opened under the old lease may still exist. DELETE /admin/leases/<id> only calls registry.releaseLease (host-lease-http.ts:76-84) and does not visibly close sessions bound to that lease. assertRequestSessionLeaseMatches could then refuse commands from the new lease against that old session. The PR's real-tool run covers open to close on one app, not stop and relaunch. Worth one manual check on the two-Mac run; if it sticks, derive the tenant from the lease id or app attempt.
5. Non-blocking: doctor finding text is now wrong (packages/stim-cli/src/device-host/agent-driver.ts:12-15)
It still says "agent-device starts only once it can lease a single macOS app" and the fix reads "once agent-device can lease one macOS app". The setting guide and website were updated to feature detection, but this finding was not.
6. Non-blocking: guidance does not tell an agent how to use the route (packages/stim-cli/src/guide/macos.ts:185-205, website/docs/macos.md:190-196)
- The agent-device rows say they "work once the host's agent field names agent-device". With a
macos-applease the agent must first runagent-device open <host.bundleId> --remote-config <path>, and only the allow-listed commands work (open close snapshot diff wait find get is click fill press type focus scroll screenshot batch). Installs, other apps,--surface desktopand full-screen capture are refused. None of that is stated. - The client's own agent-device must also know
leaseBackend: macos-app. An older one rejects the remote config on the enum, which gives an opaque error. This is not documented or checked. STIM_AGENT_DEVICE_BINis documented as "in stim-server's environment" without saying how to set it for the LaunchAgent (stim-server service install --env). The PR body does name that, so put it in the README or guide.STIM_AGENT_DEVICE_BINis a new machine-level override that is not insettings-registry.ts. CLAUDE.md says every setting and its environment override is defined there. Either register it or note why it is an env var only.
7. Test gaps for real failure modes
- No test for path,
runtimeormetafields in a client request (finding 1). - No test for a body over
MAX_RPC_BYTES(expect 400) or a client that aborts mid-body. - No test that a failed renewal or a failed
DELETEon revoke leaves a consistent state (findings 2 and 3). - The restart path (daemon exits,
HostedAgentHost.lost, re-issue, old renew timers cleared) is covered only against a fake driver inagent-driver.test.ts.AgentDeviceDriver.stop()clearingleasesand the timers on that path has no direct test.
Minor, no action needed: readBody keeps reading an oversized body to the end instead of destroying the request, and has no read timeout.
What I checked and found sound
- Route allow list:
/rpcPOST,/healthGET and/artifacts/...GET only; the..and encoded-path cases are refused. - The client cannot name another tenant, lease or session through
meta. The daemon honorsmeta.sessionIsolationandtenantIdwithout an auth hook, and the pin overrides both.flags.tenantloses tometa.tenantId.params.sessionis prefixed with the tenant. - The client credential and headers are never forwarded.
- Fail-closed behavior: an older agent-device rejects the unknown
leasespolicy key, and a missingmacos-appin/healthtears the daemon and claim down. parseHostedAgentGrantand the protocol schema agree, including the pid-pinneddeviceKeypattern.
|
Addressed in 2e5bc9f (Stim) and in the agent-device branch (callstack/agent-device#3236, 4e13a8d2a):
|
janicduplessis
left a comment
There was a problem hiding this comment.
Review clear. Re-checked 2e5bc9f and the agent-device branch at 4e13a8d2a by reading the code; I did not run the daemon. Two non-blocking items below, one of them worth closing on the agent-device side before callstack/agent-device#3236 merges.
Earlier findings
- 1 (host paths and runtime hints): resolved on both sides.
- Stim:
pinLease(packages/server/src/agent-device-driver.ts:526-558) now buildsmetafromrequestId,debug,includeCost,responseLevel,sessionExplicitandclientArtifactPathsplus the owner fields, and dropsparams.runtime. The daemon'scommandRpcParamsSchemaaccepts onlytoken/session/command/positionals/input/flags/runtime/metafrom the wire, so nointernalor other field gets through. - agent-device:
assertInvocation(ad/src/daemon/macos-app-lease.ts:149-163) refusesHOST_INPUT_KEYSon flags and input for every command,runtime.launchUrlandruntime.bundleUrlon the request, and any screenshot target that is not the client's/tmp/agent-device-screenshot-<ts>-<id>.pngtemp name. Each batch step re-enters admission with its ownruntime, flags and input (session-batch.tscallsinvoke), so a step carrying a host path orruntime.launchUrlis refused when it runs.diffis off the allow list in both places and the two lists match. clientArtifactPathsis the one client-supplied path left inmeta. The daemon only echoes it back as the artifact'slocalPathand the client downloads to it on its own machine (request-finalization.ts:122,daemon-artifacts.ts:404). The daemon never writes there, so it does not reach the host.
- Stim:
- 2 (revoke after put): resolved.
issue()now revokes first (agent-device-driver.ts:414). If the DELETE throws,issue()rejects with nothing allocated and the old lease expires on its TTL. - 3 (renewal failure only logged): the reply is reasonable. A failed PUT re-creates the lease on the next tick, and a dead daemon goes through the exit watcher.
- 4 (sessions across a relaunch): deferred to the two-Mac run. Fine, but keep it on the list.
- 5 (doctor text) and 6 (guide, website,
STIM_AGENT_DEVICE_BIN): resolved. TheSTIM_AGENT_DEVICE_BINanswer holds; it is a stim-server process variable likeSTIM_BIN, not a project or machine setting. - 7 (tests): the meta and runtime pinning test covers
cwd,developerDir,installSource,lockPolicyandruntime.launchUrl. The oversized-body, failed-DELETE andstop()-after-restart cases are still untested; not blocking.
Does dropping meta break a normal client flow? No. In the daemon, meta.cwd is used to resolve relative host paths, as the session scope root for non-tenant sessions (tenant isolation is forced here), and as the workspace of a device claim, which a macos-app open skips (session-open-execution.ts). The client sends cwd, lockPolicy and lockPlatform on every request; none matters for open close snapshot wait find get is click fill press type focus scroll screenshot batch. installSource and uploadedArtifactId only apply to install, which is refused. developerDir is never sent to a remote daemon. A remote screenshot already rewrites its target to the /tmp/agent-device-screenshot-<ts>-<id>.png form (daemon-artifacts.ts:249-262), so it matches the new admission regex, and clientArtifactPaths is kept so the artifact download lands at the client's chosen path. The platform: 'macos' in the CLI's remote config still passes assertAppSurface.
Non-blocking findings
- A client can omit
platformand select a device. Themacos-appadmission checksflags.platformonly when it is present (ad/src/daemon/macos-app-lease.ts:206-217), andassertSessionIsLeasedAppruns only when a session already exists. A firstopen <bundleId>carryingflags.udid,serialordevicewith noplatformtherefore passes admission and goes toresolveTargetDevice(session-device-resolution.ts:28-37), which can resolve an iOS simulator or Android emulator on the hosting Mac. After that the session is refused by the device check, andopenof the macOS app's bundle id will normally fail on a device where it is not installed, so I rate the impact low. It could still boot a host simulator, and I did not verify that end to end. Fix in agent-device: refuseudid,serial,device,iosSimulatorDeviceSet,androidDeviceAllowlistandtargetunder amacos-applease, or requireplatformto bemacosrather than allowing it to be absent, and re-check the resolved device on a newopen. A cheap Stim-side mitigation is forpinLeaseto setflags.platform = 'macos'and delete those selectors from the top-levelflags, but a batch step's own flags override the parent's, so the agent-device check is the one that covers it. Add a case tomacos-app-lease.test.ts. - The whole-batch check does not read
step.runtime(ad/src/daemon/macos-app-lease.ts:129-136). A step withruntime.launchUrlis still refused when it runs, but the earlier steps have already executed, so the "no step runs before a later one is refused" comment is not fully true. Checkstep.runtime?.launchUrlandbundleUrlin the pre-pass.
janicduplessis
left a comment
There was a problem hiding this comment.
Fresh review of 2e5bc9f (Stim) against the agent-device branch at 4e13a8d2a (callstack/agent-device#3236). I read both sides and ran the agent-device checkout (proxy with the exact policy, native backend, host-allocated leases through /admin/leases, then RPCs shaped like pinLease output) to check the items below. I did not re-report the already-fixed host path and runtime hint finding.
Confirmed issues
1. Blocking: a client can select host devices; pinLease does not pin platform (packages/server/src/agent-device-driver.ts:529-540)
The command branch pins meta but passes flags, input and positionals through unchanged. agent-device's macos-app admission only checks flags.platform when it is present (macos-app-lease.ts:196), and assertSessionIsLeasedApp runs only when a session already exists. A client that sends a device selector and no platform escapes the lease. Against the live daemon, with a valid lease and a pinned meta:
open <leased bundle id>withflags: { udid: <booted simulator UDID> }reachedxcrun simctl launchon that real simulator ("Simulator device failed to launch ..."). The launch failed only because the bundle id is not installed there.snapshotwith no session andflags: { serial: 'emulator-9999' }returned "No Android, HarmonyOS device, or Vega VVD with serial emulator-9999", and withudidreturned "No Apple device with UDID ...". So the lookup runs against the host's device inventory before anything lease-related refuses it. With a real serial, the allowed sessionless commands (snapshot,click,type,press,screenshot) would run against that device.emulator-5554is a predictable serial, andadbon a hosting Mac can also see a plugged-in phone.- Same lease,
flags: { platform: 'macos', serial|udid: ... }is refused ("--serial selects Android ... but this request selected --platform macos", "No Apple device with UDID"). A batch whose steps carryserialis refused the same way, because steps inherit the parent'splatform.
Exploit: an agent holding a grant for one hosted app drives other simulators, emulators or an attached phone on the hosting Mac. That contradicts the "never unscoped access" guarantee in the issue and the README.
Fix (Stim): in the COMMAND_METHODS branch, set flags: { ...flags, platform: 'macos' } after the spread and drop udid, serial, device, target, iosSimulatorDeviceSet and androidDeviceAllowlist from flags and input. Forcing platform alone closed every case I tried, including the batch one. Also fix it in agent-device admission (refuse selectors under a macos-app lease, require platform === 'macos' instead of allowing it to be absent, and re-check the resolved device on a new open), since the existing allow-list stays a blacklist on the Stim side. Add a test with udid/serial and no platform in a client request. This is the same gap an earlier review listed as low impact and unverified; it is reachable.
2. Non-blocking, fix in this PR if cheap: params.command is not checked, so commands.allow does not bound what runs (agent-device-driver.ts:53-79, 529-540)
agent-device's policy treats protocol commands (lease_*, session_list, session_save_script, release_materialized_paths, human_control) as outside commands.allow (daemon-policy-file.ts PROTOCOL_COMMANDS), and lease_*, session_list and release_materialized_paths skip lease admission. Stim lets any params.command through the agent_device.command method. Verified on the live daemon: command: 'session_list' returned 200 and command: 'lease_release' released the lease ({"released":true}) with all pinning in place; install, devices and artifacts were correctly denied. Impact: the client can end its own lease (every call then fails with LEASE_NOT_FOUND until the next 2 minute renewal PUT re-creates it), and session_list returns sessionStateDir and runnerLogPath, which are host paths. The comment on POLICY ("only these commands run at all") is therefore not true for what Stim exposes.
Fix: in pinLease, refuse a command RPC whose params.command is not in POLICY.commands.allow, and do the same for each flags.batchSteps[].command. Then the README line "admits only the commands that drive one app" is true.
3. Non-blocking: revoke can lose a race with an in-flight renewal (agent-device-driver.ts:417-434)
revoke() clears the interval and sends DELETE, but a renewal PUT already in flight can reach the daemon after the DELETE. putHostLease creates a missing lease, so the revoked lease reappears for up to 10 minutes with nobody renewing it. A request that was authorized before the revoke and is still reading its body (relayPinned captured lease) can also complete against it. The window is narrow and the pid-pinned lease dies with the app, so I rate it low. Fix: track the in-flight renewal promise on the Lease and await it before sending DELETE, or send DELETE twice.
Minor notes
- Every daemon error response carries
logPathanddiagnosticsRecordwith absolute host paths (seen on every 401/500 above). The forward could stripdata.logPathfrom JSON-RPC errors, or leave it and say so in the README. POLICYhas nocapabilities.deny: ['device-shutdown']. Withplatformpinned this is probably moot for a macOS target, but the key is already supported by the same agent-device version and costs one line. I did not verify whatclose --shutdowndoes on a macOS target.readDaemonparsesdaemon.json, which holds the daemon token. If the file is truncated, Node 22'sSyntaxErrormessage includes a snippet of the source, andHostedAgentHost.reasonwriteserror.messageto stderr. Unlikely and short, but it is the one path I found where token bytes could reach a log.- Test gaps for real failure modes: nothing sends
udid/serialwithoutplatform(item 1) or a non-allowedparams.command(item 2); no test of a body overMAX_RPC_BYTESreturning 400; no test that a failed DELETE on revoke leaves a consistent state.
What I checked and found clean
- Route confinement.
DAEMON_PATHSis anchored and its segment class excludes%,\,;,?;..is refused; query strings are forwarded but cannot change the route (the proxy routes onURL.pathname)./rpc/.and/health/.normalise to routes the proxy 404s;/artifacts/.normalises to the tenant inventory. POST is required for/rpcand GET for the other two, so HEAD and method override headers do nothing (override headers are not forwarded).server.tsonly dispatches URLs matching^/device-host/agent/<36 hex>with[/?]or end after it, so absolute-form and//URLs never reachforward, and a session id that is a prefix of another cannot match (the remainder must start with/). - Methods. Only
agent_device.commandand lease heartbeat/release (both spellings) pass;lease.allocate,install_from_source,release_materialized_pathsand array bodies get 400.metais an allow-list; the daemon'scommandRpcParamsSchemalimits top-level params, and batch steps accept onlycommand, positionals, input, flags, runtime, so a step cannot carry its ownmetaor session.leaseScopeFromRequestprefersmetaoverflagsfor tenant, run, lease, provider, device key and client, and all are pinned.scopeRequestSessionprefixes the session with the pinned tenant, so namingstim.other:defaultbecomesstim.mine:stim.other:default.lease_allocateformacos-appis refused by the daemon even if named through the command method. - Tenant and artifacts. The forward sets
x-agent-device-tenantfrom the lease and never forwards a client one; the proxy forwards that header andcanReadArtifactfilters by it, for both/artifacts/inventory and/artifacts/<id>. Every Stim request carries a tenant, so the "no tenant means readable" branch is not reachable from a grant. - Token handling. The shared proxy token and the daemon admin token are different. Only the proxy token is used on forwarded requests, set from the host side; the client's
Authorization,x-agent-device-token, cookies andparams.tokendo not reach the daemon as authority (the proxy rewritesparams.tokento its own). The daemon token is read fromdaemon.json(mode 0600 in a 0700 dir), is used only on the loopback/admin/leasescall, and appears in no argv, state file, response or the messages the client sees (non-AgentDriverUnavailableerrors become a generic string). The proxy token is in the child's environment only. - Inertness and fail-closed.
hosting.agentDrivernoneconstructs no driver; withagent-device, a daemon withoutmacos-appin/healthupstream.leaseBackendsis torn down with its claim removed, and grants are{ driver: 'none' }with a notice. A missing or expired lease gives 503 inforward(no fall-through to an unscoped call); an expired lease at the daemon givesLEASE_NOT_FOUND. - Lifecycle.
HostedAgentHostserialisesappRunning,appStoppedandlost, so concurrentissue/revokefor one session cannot interleave. Failed allocation leaves no lease or timer and stops the daemon if nothing else is live. Stop, app removal and device-host revocation all go throughappStoppedordrop; a crashed daemon clears the lease map instop()and re-issues new grants with new tokens, and the old grant gets 503 meanwhile.STIM_AGENT_DEVICE_BINpins the binary and fails with a clear message when unreadable. - Docs. The settings guide, website and README match the implementation for feature detection,
STIM_AGENT_DEVICE_BIN,stim-server service install --envand the command list, apart from the "only the commands that drive one app" claim, which needs item 2.
Not tested: a real run through a physical host, and relaunch of the same app under the same tenant (deferred to the two-Mac run). The one real-device interaction I made was the failed open against a booted simulator in item 1; I ran nothing against the attached Android phone.
…pp leases The agent-device adapter starts only when the daemon advertises the macos-app lease backend, allocates a host lease per hosted app process over agent-device's loopback admin route, renews and releases it, and pins every forwarded command to that lease. The client writes the lease into its remote config. STIM_AGENT_DEVICE_BIN names the agent-device binary. Refs #2477.
The forward keeps only reporting metadata from the client, drops runtime hints, revokes a session's old lease before allocating a new one, and drops diff from the allowed commands. The doctor note, guide and website say how an agent starts and what the lease allows.
2e5bc9f to
c700ce2
Compare
janicduplessis
left a comment
There was a problem hiding this comment.
Verdict: clear on correctness and security. No blocking findings. Three non-blocking findings, two of them test gaps I confirmed by mutation.
Reviewed da8b2b7e8..c700ce291 in packages/server/src/agent-device-driver.ts against agent-device 5c958e109 (src/daemon/macos-app-lease.ts, packages/command-registry/src/batch.ts, src/daemon/server/http-server.ts). pnpm test packages/server/__tests__/agent-device-driver.test.ts passes (18/18) and agent-device's macos-app-lease.test.ts passes (18/18). I ran the mutations on a scratch copy; the worktree is untouched.
The three earlier findings
- Selectors and platform. Closed.
pinLeasestripsdevice,udid,serial,target,iosSimulatorDeviceSetandandroidDeviceAllowlistfrom top-levelflags, top-levelinput, each step'sflagsand each step'sinput. It forcesplatform: 'macos'on top-level and step flags. That list is the full set the daemon uses to pick a device (src/daemon/session-selector.tsreads exactly those six, plusplatform). The daemon builds its request fromparams.flags, soinput.platformselects nothing; the lease check merges{...input, ...flags}andflagswins.flags,inputand steps that are not objects are handled: a non-objectflagsorinputbecomes{}or is dropped, and a non-object step returns 400. - Command allow-list. Closed.
isAllowedCommandis an exact, case-sensitive match againstPOLICY.commands.allow, which is also the list given to the daemon, so the two cannot drift.Snapshotandsnapshotare refused. agent-device'snormalizeBatchCommandNameis onlytrim().toLowerCase(), so there are no aliases. Exact matching is stricter than the daemon, never looser.- A step
commandof['snapshot']orundefinedis refused, and so is a missing top-levelcommand. - Nested
batchpasses Stim's list, and the daemon refuses it (assertBatchRuntimeCommandAllowed). lease_heartbeatandlease_releaseuse their own RPC methods (LEASE_METHODS) and never go throughcommand, so the allow-list does not touch them. The client builds them that way inbuildHttpRpcPayload.install_from_sourceandrelease_materialized_pathsstay refused.
- Revoke racing a renewal. Closed.
lease.renewingholds the latest renewal promise. The promise ends in.catch, so awaiting it never rejects and cannot leave an unhandled rejection. It also never waits onrevoke, so it cannot deadlock.adminRequesthas a 10s socket timeout, so the await is bounded. The renew interval is 120s, so two renewals do not overlap and tracking only the latest is enough.this.runningis read after the await, which is correct if the daemon stops meanwhile.stop()clears the intervals and the lease map without awaiting. A renewal still in flight then fails against the stopping daemon and is caught and logged. That path is acceptable.
I also checked {"__proto__": {...}} keys. JSON.parse gives an own data property, and the spread, Object.fromEntries and rest destructuring all keep it as an own property. It does not reach any prototype and flags.udid stays undefined. I found no Object.assign or target[k] = v copy of request flags in agent-device (not an exhaustive audit). Not exploitable as far as I can tell. See finding 3.
Findings (all non-blocking)
-
The renewal-await has no test that would fail without it.
agent-device-driver.test.ts:181-215(the revoke test withleaseRenewMs: 50) passes 3/3 withawait lease.renewing;removed (agent-device-driver.ts:439). It only fails if the interval happens to fire within a few milliseconds ofrevoke. Failing input: make the fake admin PUT slow (say 100ms), callrevokewhile the PUT is in flight, and assert that the last admin call is theDELETE. Without the await the PUT lands after it. -
The input and step-flag selector stripping is not asserted.
agent-device-driver.test.ts:404-423usestoMatchObject, which accepts extra keys. Three mutants all pass 3/3:- top-level
inputnot stripped (input: { udid: 'SIM-3', text: 'hi' }still matchesinput: { text: 'hi' }); - step
inputnot stripped (the expectationinput: {}matches any object); - step
flagsnot stripped (flags: { udid: 'SIM-2', platform: 'android' }still matches{ platform: 'macos' }).
The top-level
flagscheck does fail on mutation, because of theObject.keys(...).toEqual(['batchSteps','platform','surface'])assertion. Add the same exact-equality check forinput, stepflagsand stepinput(toEqual, nottoMatchObject). The other mutants fail the test as intended: top-level command check removed, step check removed,platformremoved,pinStepskipped, top-level flags not stripped, stepplatformremoved. - top-level
-
Hardening, optional.
batchStepsthat is present but not an array (for example a string) goes through unvalidated atagent-device-driver.ts:539-544. The daemon rejects it (validateAndNormalizeBatchStepsrequires an array), so it is not a hole today. The selector strip is a denylist, so any new daemon selector key would pass until it is added. Refusing a non-arraybatchStepsand dropping__proto__keys would makepinLeaseindependent of the daemon. The refusal is also a bare400 text/plain("Unsupported request."), not a JSON-RPC error, so the agent-device client will show an opaque transport error. A JSON-RPC error naming the refused command would be easier to debug.
I did not run any daemon or touch a simulator or device.
No description provided.