Skip to content

fix(contract): an exact osVersion is bounded like a component version - #895

Draft
V3RON wants to merge 6 commits into
security/status-own-leasesfrom
security/os-version-bound
Draft

V3RON wants to merge 6 commits into
security/status-own-leasesfrom
security/os-version-bound

Conversation

@V3RON

@V3RON V3RON commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

lease.request accepted any non-empty exact osVersion. When the driver answered a downloadable missing runtime and the request had allowDownload: true, that raw string went to the installer as the component, skipping componentVersionSchema's bound. On iOS that let whitespace and trailing text into xcodebuild -downloadPlatform iOS -buildVersion <raw>. On Android, ; (sdkmanager's package path separator) could add extra segments to system-images;android-<raw>;....

An exact osVersion must now pass componentVersionSchema, and that schema now also refuses ;. Ranges are parsed as before.

Changes

  • src/contract/operations.ts: componentVersionSchema moves above the lease request schema and its regex also refuses ;. refuseBadOsVersion checks an exact constraint against componentVersionSchema. If it fails, the request gets a 400 BAD_REQUEST: "an exact OS version is 1 to 64 characters, none of them whitespace, a control character or ;, and does not start with -" (path osVersion). Range parsing is unchanged.
  • src/contract/protocol.ts: PROTOCOL_VERSION_RANGE 57/57 -> 58/58. The manuals that state it are updated (src/contract/AGENTS.md, src/daemon/AGENTS.md), and src/contract/AGENTS.md now states the bound.
  • website/.../using-devices/choosing-a-device.mdx: says "any other text is an exact version", so it now adds the bound in one sentence. reference/cli.mdx and programmatic-client.mdx only say "an exact version or a range", so they are unchanged.
  • src/daemon/server.test.ts: the test for a component name longer than the progress push's 64-character limit used to send a 65-character osVersion, which is now refused. Now the fake driver names the long component itself (RuntimeMissingError with component: "9".repeat(65)), and the request sends osVersion: "26.5". It is the same path through the server.
  • src/daemon-protocol/index.test.ts: expects 58.

Tests

  • src/contract/operations.test.ts (strict area): a new table test refuses the exact versions "17.0 -foo", "-1", "34;extras", a 65-character string, a string with a control character and " 17.4", each with the exact message and path. The accepted table adds "17.4", "34", "18.5.1" and a 64-character version. The range forms (>=18, >=18 <26, 18 - 26) still pass. The component.install refusal table adds "35;extras". All of these failed on the test commit and pass with the fix.
  • Published surfaces: src/mcp/server.test.ts, src/http/app.test.ts, src/http/errors.test.ts, src/http/openapi.test.ts, src/cli/lazy-loading.test.ts, src/cli/index.test.ts pass.
  • pnpm test:changed: 140-141 of 142 files pass. Each run had 1-2 timing failures, a different test each time, all in files this PR does not touch: the CLI 20 s startup deadline, usage reader ordering, and dispatcher device.exec chunk delivery. Each of those files passes when run alone. The machine load average was 14-24 from other agents. The same 142 files passed twice on base 3ca05c56b.
  • pnpm test:console --project=chromium: 207 passed and 2 failed. The drain/Undo failure is the known flaky test Console e2e: a worker is drained from Fleet with no dialog, and Undo undrains it fails on the row's drained text #886. "Stop stops the capture" timed out under load and passed when screen.spec.ts was rerun.
  • Website build and check-openapi.mjs: ok (30 operations). openapi.json is generated at build time, so it has no checked-in diff.

Assumptions

  • Assumption: min moves to 58 along with max. No compatibility path stays side by side: an older client only differs in that a malformed exact osVersion is no longer accepted.

Written by an agent.

V3RON added 2 commits October 22, 2026 17:12
lease.request took any non-empty exact osVersion, and a downloadable
missing runtime handed it to the installer as the component, past
componentVersionSchema's bound. An exact osVersion is now held to that
schema, which also refuses ';' (sdkmanager's package path separator).
Ranges keep their grammar. Protocol 54 -> 55.
@V3RON

V3RON commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Review (maintainer's agent, round 1)

No blocking findings.

  • The bound is applied by reusing componentVersionSchema inside refuseBadOsVersion for the exact form only, so one regex governs both component.install and a downloadable osVersion; ; is now refused in both, which closes the sdkmanager package-path injection on Android, and the leading-- refusal covers xcodebuild -buildVersion on iOS. Range parsing is untouched, and range versions are read numerically so they never reach an installer as text.
  • The error names the field (path: ["osVersion"]) and states the rule in words a user can act on. The website's choosing-a-device page, the one place that said "any other text is an exact version", now states the bound; Baklava and 34-ext12 still pass it.
  • Protocol 54 → 55 with min moved too, as there is no compatibility path. feat(gateway): a join token is pinned to the first worker that uses it #894 bumps to 55 as well; whichever merges second rebases and bumps to 56.
  • The server.test.ts change is legitimate: that test needed a 65-character component only to exceed the progress push's name limit, and it now gets it from the fake driver instead of a request the contract rightly refuses.

Note, no change needed: the one-off timing failures across test:changed runs were in untouched files under a load average of 14 to 24 from parallel agents and pass alone and on base; CI is the arbiter.

@V3RON
V3RON changed the base branch from main to security/status-own-leases October 23, 2026 06:19
@V3RON
V3RON added this pull request to stack #897 October 23, 2026 06:19

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant