Skip to content

feat(lease): name the iOS simulator mode in a proxy lease device spec - #4183

Merged
thymikee merged 8 commits into
callstack:mainfrom
vitalyiegorov:feat/lease-device-mode
Nov 5, 2026
Merged

thymikee merged 8 commits into
callstack:mainfrom
vitalyiegorov:feat/lease-device-mode

Conversation

@vitalyiegorov

@vitalyiegorov vitalyiegorov commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A proxy allocate can now ask for a slim or full iOS simulator. deviceMode joins osVersion, imageTag and physical as a device-spec key (ADR 0007, amended). The CLI's --device-mode, the SDK's leases.allocate({ deviceMode }) and MCP send it on agent_device.lease.allocate. A daemon refuses it with device-spec-unsupported, like the other keys, and it is excluded from env defaults.

Why: simlock leases slim simulators by default, and Apple Pay / Wallet (PassKit) flows need a full one. com.apple.passd is disabled on slim.

It is named deviceMode, not mode: mode is back --mode in-app|system, and a spec key named mode would make the daemon refuse those commands.

23 files. Backend side: callstackincubator/simlock#1225 (issue callstackincubator/simlock#1223).

agent-device open MyApp --platform ios --device-mode full

Validation

Tested SHA 21590b94705d74a807b92d604b194ea595d7a488, rebased on main 9cf8168a4.

  • check:affected --run --head 21590b947: stopped on the smoke-daemon-clean retirement flake (3 of 8 isolated failures on main), so no single receipt. The 14 checks before it and the 26 blocked ones, run through pnpm gate, passed. 97f95411c had a full passing receipt; only a test and help text changed since.
  • Live at 12580a6aa, the last production change, through a real gateway (transcript, RPC, Wallet): the allocate sends deviceMode: "full", the farm records mode: full, com.apple.passd runs.
  • The refusal is not reachable today. No gateway reports the mode yet: feat(agent-device): the allocate reply reports the granted device mode callstackincubator/simlock#1254 is an open draft and is not deployed. Its only coverage is unit tests and a stub gateway.
  • Guarantee: slim reported for full is refused, full for slim is served, and a missing mode is accepted unconfirmed.

🤖 Generated with Claude Code

View guided diff

@vercel

vercel Bot commented Nov 3, 2026

Copy link
Copy Markdown

@vitalyiegorov is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

@thymikee

thymikee commented Nov 3, 2026

Copy link
Copy Markdown
Member

The open --device-mode full route works in the code at 69cfd20, but no test covers the CLI or MCP half of it. There is no conflict reported, and the only check on this head is the Vercel deployment authorization notice for a fork PR, which does not build this diff, so no CI job has exercised the lease route yet.

The route goes CLI flag, then allocateOrReuseLease, then bindLeaseNamedDevice, then the command. Three new lines carry it and no test reaches them: delete flags.deviceMode in proxy-lease-device.ts, the deviceMode: _deviceMode destructure in connection-lease-transaction.ts, and the profile default in proxy-profile.ts. If someone deletes delete flags.deviceMode, the daemon would refuse open --device-mode full with device-spec-unsupported, and the suite would stay green. From my reading, the tests you extended (SDK allocate, readLeaseDeviceSpec, router gate) never touch these lines; I did not run the tests or mutate the code. Please extend the existing cases instead of adding new ones: add deviceMode: 'full' to the open-by-spec case in proxy-lease-device-spec.test.ts and assert the request carries it, the materialized flags do not, and the recorded leaseDeviceSpec.spec does. Do the same for the connect proxy defaults case, including the blank case, and add the key to the MCP advertise list in command-tools-operator-inputs.test.ts. Iterating LEASE_DEVICE_SPEC_KEYS in those assertions would let the next key pass without a test edit.

Not blocking, and fine to take or leave: LEASE_DEVICE_SPEC_KEYS in contracts/lease-scope.ts could replace the hand-written key lists in connection-lease-transaction.ts and proxy-lease-device.ts, which have no compile-time check. Also, the granted LeaseDevice does not echo the mode, so a backend that ignores deviceMode would return a slim simulator without an error, as it already can for imageTag and osVersion. A separate issue for that is enough, and the docs can say the backend must support the key.

I did not require a device run, since no device-facing daemon path changed, and I cannot reproduce the live simlock numbers. Before merge, the tests above need to land, and a maintainer needs to authorize the Vercel deploy and approve the fork CI run. The unit failure in smoke-daemon-clean.test.ts that the PR body mentions does not touch this diff, but I could not read that log.

@vitalyiegorov
vitalyiegorov marked this pull request as ready for review November 3, 2026 12:20

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 21 files

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

Comment thread docs/adr/0007-remote-device-leases.md Resolved
vitalyiegorov and others added 2 commits November 4, 2026 07:36
A backend in front of the daemon (simlock) can lease an iOS simulator slim
or full, but a proxy allocate could not ask for one: the device spec had
no key for it, so every lease got the backend's default. Apple Pay and
Wallet (PassKit) flows need a full simulator on an otherwise-slim farm.

`deviceMode` joins osVersion, imageTag and physical as a device-spec key: the
CLI's --device-mode, the SDK's leases.allocate and MCP send it on the allocate,
and a daemon refuses it with device-spec-unsupported like the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open --device-mode route had no test: deleting the spec key from the
materialized flags, the non-by-spec allocate or the connect proxy profile left
the suite green. Extend the open-by-spec, re-allocate, profile and MCP cases and
iterate LEASE_DEVICE_SPEC_KEYS. The two hand-written key lists in the CLI become
loops over the same constant, so the next key needs no edit there.

lease.device does not report deviceMode; the docs now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vitalyiegorov
vitalyiegorov force-pushed the feat/lease-device-mode branch from 69cfd20 to 30454ea Compare November 4, 2026 06:48
@vitalyiegorov

Copy link
Copy Markdown
Contributor Author

Ready for another look at 30454ea (rebased on main). The open-by-spec, re-allocate, connect proxy (with blank) and MCP cases now cover deviceMode and iterate LEASE_DEVICE_SPEC_KEYS. I took your optional suggestion: the two hand-written key lists are loops over that constant. The docs state that lease.device does not report the mode. The fork run still needs a maintainer to approve CI and Vercel.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

vitalyiegorov added a commit to vitalyiegorov/simlock that referenced this pull request Nov 4, 2026
… granted mode (callstackincubator#1223)

No released agent-device has `--device-mode` (callstack/agent-device#4183 is
open), so the manual and website name the field the front reads. The edge
test also checks the mode of the granted device, which `lease.requested`
cannot show: it is logged before the core resolves the mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ested

lease.device did not report deviceMode, so a backend could lease a slim
simulator for a full request with no error. The lease can now report the
granted mode, and the CLI proxy connection releases a new lease that reports
another mode than the request named, and fails with device-spec-unsupported.

A lease that reports no mode is still accepted, because no simlock reply reports
one yet and refusing it would break valid full leases. The docs state that
the mode is then unconfirmed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At 30454ea, the new CLI and MCP test covers the open --device-mode full route I asked about, and the production commit is unchanged since your live run, so that run still covers it.

The mode reporting thread still applies, as do two lower-priority threads. Please address or reply to them before merge.

The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known.

@vitalyiegorov

Copy link
Copy Markdown
Contributor Author

Updated to bb602c0. Since my last note: a new lease that reports another deviceMode than requested is now released and refused, and the spec loops name every key.

This goes beyond your note that a separate issue was enough, so tell me if you would rather split it out. It does not close the gap: a backend that ignores the key and reports no mode still leases its default, and the docs say that. Requiring the mode has to wait until simlock reports it.

@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At bb602c0, the earlier review's reported mismatch is now refused, but the mode-proof gap and live validation remain.

Could you record the commands and output for a live connect proxy and open --device-mode full run through the gateway to a real simulator, including the allocation request, granted device identity and evidence of full mode? Please also show a gateway response reporting slim for a full request, the typed refusal and the owned lease release outcome. The earlier gateway run predates the refusal and has no output, so it does not establish either route at this head. Allocation route

Could the granting backend own and report a typed mode as part of its allocation guarantee, with requested and granted modes validated at the shared lease allocation boundary and the fact required once the backend supports it? This would first need a coordinated simlock response contract and migration, while retaining lease ownership-aware cleanup at the caller.

The mode-proof thread still applies, as does one lower-priority thread. The fixture thread no longer applies because the required fixture now supplies imageTag.

Not blocking: The documentation can qualify that a mismatched grant is released unless the allocation explicitly reports reuse; take or leave this suggestion.

The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known. Before merge, please post the live run at this head and settle the mode-proof question.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comparing the modes for equality would release a valid lease once a backend
reports the mode: simlock's ADR 0007 makes full a guarantee and slim best effort,
so a slim request can be granted a full simulator and that is never an error.
Only slim granted for full breaks the request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vitalyiegorov

Copy link
Copy Markdown
Contributor Author

Live run at 12580a6. Commands and output are in the transcript, and the RPC is in the log. A local relay between the CLI and the gateway recorded it.

Route: this head's CLI on Linux, then https://simlock.vitaliiyehorov.dev/v1/agent-device, then a real iPhone 17 Pro simulator on iOS 26.2. The daemon behind the gateway is agent-device 0.21.36, not this head; this PR's logic is on the client.

  • Allocation request: agent_device.lease.allocate with platform: "ios", device: "iPhone 17 Pro", deviceMode: "full".
  • Granted identity: deviceKey: ios:mobile:8C5BE9FB-A00B-49F5-B6D8-86302DCA1635, lease.device { model: "iPhone 17 Pro", osVersion: "26.2", physical: false }. The reply reports no mode, so the client accepted it unconfirmed.
  • Full mode, from outside that reply: the farm's own record of the lease says mode: "full". launchctl print system/com.apple.passd shows state = running, pid 16682, and Wallet opens. There was no slim control in this run.
  • Cleanup: close ended the lease, and the token held 0 leases afterwards.

Slim reported for a full request: a stub, not a real gateway. The real gateway does not report a mode yet, so a local stub stands in (transcript, stub).

$ agent-device open com.example.app --platform ios --device-mode full --json
UNSUPPORTED_OPERATION, reason device-spec-unsupported, keys ["deviceMode"]
owned lease (reused: false): details.released: true, after one agent_device.lease.release
reused lease (reused: true): same refusal, no release call

The check is not equality, which changed since bb602c0. Only slim granted for full is refused. simlock's ADR 0007 makes full a guarantee and slim best effort: §4 says a slim request can resolve to full (iOS below 18.5, or a driver that does not slim) and "it is never an error", and §8 says "A slim request may be granted mode: "full". A full request is never granted mode: "slim"." Equality would have released valid leases once simlock reports the mode. full granted for slim being served is covered by a unit test, not by these runs.

Mode proof: yes, the granting backend should own and report a typed mode. The simlock half is open as a draft: callstackincubator/simlock#1254, which reports lease.device.deviceMode. Proposed agent-device side, kept minimal:

  1. Validate requested against granted at the shared lease allocation boundary, so SDK and CLI allocations both get it, using the rule above.
  2. Require the fact once the backend advertises support, through a version or capability gate. Until then a missing mode stays accepted and unconfirmed, as now.
  3. Keep ownership-aware cleanup at the caller: the boundary raises the typed failure, and the caller's ledger decides whether to release.

Should that requirement go in this PR, or in a follow-up after simlock ships? I have not added it, so this PR doesn't grow unasked.

The doc qualifier is taken, in one line of the ADR. Replied on the remaining lower-priority thread.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread docs/adr/0007-remote-device-leases.md Resolved
Comment thread website/docs/docs/remote-proxy.md Resolved
A refused allocation that reports reuse is not tracked for release, so it has no
release and no details.released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vitalyiegorov

Copy link
Copy Markdown
Contributor Author

The head moved to 97f9541 with a docs-only commit: it qualifies the release claim for a reused allocation in the ADR and the remote-proxy docs, which also covers your optional qualifier. I did not repeat the live run. It stays attributed to 12580a6aa, the last code change.

@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

At 97f9541, the narrowed refusal reads right: only a slim grant for a full request is refused, and the docs now say a refused grant that reports reuse is not released. The ADR and remote-proxy reuse threads are fixed at this head.

The real gateway run returns no deviceMode, so the refusal branch has only run against a stub. If a gateway that reports the mode is available, please run this head through it and show the full request, the slim grant, the device-spec-unsupported refusal and the release outcome. If none reports the mode yet, please say so in the PR body, so it is clear the refusal is not reachable today and the stub test is its only coverage.

Not blocking, take or leave: the help text's release and details.released wording could exclude explicitly reused allocations (cli-help.ts). One lower-priority thread still applies.

The only failing check is the Vercel deployment authorization, which is not a code problem. No conflicts are known.

vitalyiegorov and others added 2 commits November 4, 2026 11:39
deviceMode was sent on an Android allocation. The forwarding held, but the request
was not a valid one, so the mode moves to an iOS fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vitalyiegorov

Copy link
Copy Markdown
Contributor Author

Updated to 21590b9.

  • No gateway reports the mode yet: feat(agent-device): the allocate reply reports the granted device mode callstackincubator/simlock#1254 is an open draft and is not deployed, so the refusal is not reachable today and its only coverage is unit tests and the stub. The PR body says so.
  • Help text: took your clause, so release and details.released exclude explicitly reused allocations.
  • Fixture thread: deviceMode now has its own iOS case.
  • Gate: check:affected stopped on the retirement flake, which also fails on main, so there is no single receipt at this head. The checks it blocked passed through pnpm gate. 97f95411c had a full passing receipt, and only a test and help text changed since. I did not repeat the live run.

One open question from my earlier comment: should requiring the reported mode go in this PR, or in a follow-up after simlock ships? I would default to the follow-up.

@thymikee

thymikee commented Nov 4, 2026

Copy link
Copy Markdown
Member

Thanks, at 21590b9 the code looks good and this settles my earlier request. The PR body now says no gateway reports the device mode until callstackincubator/simlock#1254 ships, so the slim-for-full refusal is a guard for that backend, covered today by the unit tests and the stub. The new iOS allocation case fixes the fixture thread, and the help text now excludes reused allocations from the release claim.

On your question: a follow-up after simlock ships is right. Requiring the reported mode before any gateway sends it would only break today's allocations.

The gate is fine as well. 97f9541 had a full passing receipt, and only a test and help text changed since. The retirement flake that stopped check:affected here also fails on main. The only failing check is the Vercel deployment authorization, which is not a code problem, and no conflicts are known.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 4, 2026
@thymikee
thymikee merged commit 1846ef9 into callstack:main Nov 5, 2026
13 of 14 checks passed
V3RON pushed a commit to callstackincubator/simlock that referenced this pull request Nov 5, 2026
#1223) (#1225)

Closes #1223.

The agent-device front now reads an allocate's `deviceMode` (`slim` |
`full`) into the lease request's `mode`, as ADR 0007 §2 asks of every
transport. Why: Apple Pay / PassKit (Wallet) flows need a full iOS
simulator on a farm whose `ios.defaultMode` is `slim`, and through
agent-device every lease got the default.

- `leaseCallFields` reads `deviceMode`; `planOf` puts it in the plan as
`mode`, so `lease.request` and `lease.requested` carry it as asked.
- A bad `deviceMode` reaches `lease.request`, whose schema is the one
check, and answers `refused by simlock: mode: Invalid option: expected
one of "slim"|"full"`. `deviceMode: null` is absent, like `osVersion`,
`physical` and `ttlMs`.
- The allocate forwarded to the agent-device daemon drops `deviceMode`,
and `ruleLeaseKeys` removes it from commands and batch steps, as for
`deviceBaseline`.
- The test is in its own `device-mode.test.ts`, because `edge.test.ts`
may only shrink (ADR 0063).
- Website page and module manual updated.

Client side: callstack/agent-device#4183 is merged and unreleased, so
the docs say `--device-mode` needs the release that adds it.

### Status

- [x] Allocate with `deviceMode` full or slim makes a lease request
whose `mode` is that value and grants a device of that mode —
`device-mode.test.ts`; removing the read turns it red
- [x] Invalid `deviceMode` refused with simlock's message, and no lease
is made; `null` gets the default — same test
- [x] The forwarded allocate and every command or batch step carry no
`deviceMode` — `device-mode.test.ts`; removing either strip turns a test
red
- [x] `pnpm exec vitest run --project unit src/agent-device/
src/source-hygiene.test.ts` — 518 passed, 1 skipped, at dd3237b;
typecheck and `oxlint` clean
- [x] Real gateway + worker (0.5.0 with this patch, before the refusal
moved to `lease.request`), raw `/v1/agent-device/rpc`: `deviceMode:
"full"` → lease `mode: full`, 0 disabled launchd labels; no `deviceMode`
→ `mode: slim`, 170 disabled (incl. `com.apple.passd`). Not repeated on
this head

## Assumptions

- Assumption: new ADR? — no; ADR 0007 §2 already decides it, this closes
a gap against it
- Assumption: field name — `deviceMode`, because agent-device's `mode`
is taken by other commands

## Review

Spec review: 0 blocking, 0 fixed, 0 notes. Code review: 0 blocking, 2
fixed, 0 notes. Claims review: 0 blocking, 1 fixed, 0 notes.
Assessment: complete
Rounds: 2. Tokens: 0.45M.
External contribution: the spec, code and claims briefs ran as separate
agents from a fork clone, not through the `reviewer` agent, so the
ledger's metrics are unknown. The round 2 code lens had no dependencies
and marked itself incomplete, so its coordinator then ran the tests and
three mutation probes above. Spec ran in round 1 only.

*Written by an agent.*

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
V3RON added a commit to callstackincubator/simlock that referenced this pull request Nov 5, 2026
#1254)

The agent-device front now reports the granted mode: `data.lease.device`
carries `deviceMode`, the mode the device has (ADR 0007 §8). Why:
callstack/agent-device#4183 can only check a requested mode if the
backend reports the grant, as [its maintainer
asked](callstack/agent-device#4183 (comment)).

- `withGrantedDevice` takes `grant.device` and adds `deviceMode` from
`device.mode`. A phone has no mode, so no key.
- Released agent-device (0.21.36) reads named fields only and ignores
it.
- A slim request can be granted `full`, and that is not an error (ADR
0007 §4, §8). A client should refuse only `slim` granted for `full`.
- Builds on #1225, now merged, which sends the requested mode; this
reports the mode the device has.

### Status

- [x] Simulator and emulator replies carry `deviceMode`, slim and full,
on a worker and through a gateway — `edge.test.ts`,
`gateway-front.test.ts`, red before the fix
- [x] A phone's reply has no `deviceMode` — `edge.test.ts`, red when a
phone is made to report one
- [x] `pnpm exec vitest run --project unit src/agent-device/
src/source-hygiene.test.ts` — 518 passed, 1 skipped, at e9e3df9;
typecheck clean. Website build and OpenAPI check not repeated on this
head
- [x] Real simulator: full and slim replies carry the granted
`deviceMode` (local slow-lane test, 2026-11-05)

## Assumptions

- Assumption: field name — `deviceMode`, not simlock's `mode`, because
`lease.device` is agent-device's reply shape and #4183 reads that key
- Assumption: no issue — this answers a request on #4183, so the branch
has no issue number

## Review

Spec review: skipped. Code review: 0 blocking, 0 fixed, 0 notes. Claims
review: 0 blocking, 0 fixed, 0 notes.

Rounds: 1. Tokens: 3M.

Spec review skipped: no issue. Code review ran behaviour and tests on
Opus; Assessment: complete. Fix-check at 2bc64d6: a merge of `main`
that keeps both sides unchanged, verified by the coordinator, no lens.

Hardware: pass. On a real iOS simulator, a local slow-lane test (not
committed) allocated with `deviceMode` "full" and "slim" on a
slim-default worker; the reply's `data.lease.device.deviceMode` matched
the granted mode both times (2.5 min).

Review ledger:
#1254 (comment)

*Written by an agent.*

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Szymon Chmal <szymon@chmal.it>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants