refactor(ios): make unscoped simctl argv a type error at the Apple tool port - #2898
Conversation
Brand scoped simctl argv in contracts (ScopedSimctlArgs) so the Apple tool port and the provider simctl executor refuse raw argv. Mint a SimulatorAddress from a DeviceInfo, make the set-scope builder take an explicit scope, and delete buildSimctlArgs. Move the log-stream and AX bridge spawns off the set-scope builder: the log stream takes the set from its DeviceInfo, and the snapshot target carries a SimulatorAddress in place of udid and simulatorSetPath. Doctor keeps set scope for 'simctl help'. No argv changes. Refs #2878
…am and network recovery Refs #2878
Refuse hand-built 'simctl' argv arrays, a '--set' literal, casts that forge ScopedSimctlArgs or SimulatorAddress, and any reference to the set-scope builder outside its owners (core/simctl.ts, simulator-inventory.ts, logs/doctor.ts). Refs #2878
Size Report
Startup median (7 runs, lower is better):
|
|
Reviewed at 2cee7ed. The code looks good: Two questions, not blocking. Does the Not blocking: the PR body says "+22 production lines", but the change is +259/-103 with the 128-line R79 rule, and it leaves out the two manual layering mutations that #2878 asks to record. I did not run typecheck or the layering check. The scoped-set live run in the PR body did not reach a clean scoped snapshot, so scoped argv equivalence rests on unit tests. Smoke, Integration, Repo Guards and Coverage were still running at review time; this diff touches their routes, so a failure there would likely be related. |
thymikee
left a comment
There was a problem hiding this comment.
Thermo-nuclear structural pass on a 36-file type-branding refactor. Genuinely good parts, said plainly: Object.freeze + mint-once simulatorAddressFor (called once, threaded rather than recomputed), buildIosSimulatorLogStreamArgs taking DeviceInfo instead of a four-field bag with a hand-passed set (deletes a real bypass), removing the in-place push on a now-frozen argv, Omit<AppleToolRequest, 'allowFailure'> -> AppleToolRequest restoring the discriminated union, the @ts-expect-error compile-time proofs matching the existing compileTimeNetworkProjectionProof idiom, and NET +22 production lines to make a class of --set-drop bug impossible. Cheap. No file crossed 1k.
The one question this PR lives or dies by is whether the brand earns its ceremony as placed. My read: as currently placed it mostly does not — the guarantee sits on one port whose own implementation discards it, so the AST gate (not tsc) carries most of the load. Three inline below. Fix #1; I'd take #2 and #3 as the follow-ups that let you shrink R79.
Smaller notes (not inline):
logs/doctor.tswritesscopeSimctlArgs(['help'], { simulatorSetPath: undefined })at a site wheredeviceis in scope, and was admitted toSET_SCOPE_OWNERSsolely to be allowed to spell thatundefined— the same "blank the set next to a device" footgun the PR set out to make loud, now permitted by path allowlist. Give the no-device case a named mint (DEFAULT_SIMULATOR_SET_SCOPE/simctlAvailabilityProbeArgs()) so no owner spellsundefinedand the allowlist collapses towardcore/simctl.ts.- The policy header claims it catches a call "scoped through the set-scope builder with the set written out as
undefined" — no such check exists; that line is legal only because of the path exemption. Fix the comment or add the check; a header that overclaims coverage is the false confidence the rule was meant to remove. - Un-export what has no consumer:
scopeSimctlArgsForAddressandSimulatorSetScopehave no importers outsidecore/simctl.tsyet are exported — andscopeSimctlArgs's exportability is exactly what R79's identifier clause must police. Alsosimulator-inventory.tsimportsScopedSimctlArgsonly to re-annotate an inferred return; drop it so the brand stops leaking into forwarder modules. scripts/is exempt from the rule andscripts/ios-snapshot-benchmark/lifecycle.tsstill hand-buildsrunXcrun(['simctl', <verb>, udid])from a bare udid — the exact shape the gate targets; the exemption is pinned by the rule's own test.
| const [tool, ...toolArgs] = args; | ||
| if (tool === 'simctl') { | ||
| return await provider.simctl.run(toolArgs, options); | ||
| return await provider.simctl.run(toolArgs as unknown as ScopedSimctlArgs, options); |
There was a problem hiding this comment.
This as unknown as ScopedSimctlArgs is the only double-cast the PR adds, and it sits at the exact spot that undercuts the thesis. The branded port has a single production implementation, and it hands the argv straight to runXcrun(args: string[]), which then reforges the brand right here. So the type error exists on the request side and is a no-op on the execution side: the brand is erased one hop below appleTools.run and re-minted unconditionally. That is also why ARGV_OWNERS has to include core/tool-provider.ts — the gate is whitelisting the forgery this placement forces. runXcrun is exported and unbranded, so every runXcrun(buildSimctlArgsFor*(...)) site is a legitimate bypass protected only by R79's ArrayExpression clause. Fix: let the tool-prefixed argv that buildSimctlArgsFor* already mints carry the brand, and split runXcrun's simctl branch to require it — that deletes this cast, deletes tool-provider.ts's ARGV_OWNERS exemption, and turns the ArrayExpression half of R79 into a tsc error.
| } | ||
|
|
||
| export function buildSimctlArgs( | ||
| export function buildSimctlArgsForAddress( |
There was a problem hiding this comment.
scopeSimctlArgs* returns ScopedSimctlArgs (branded), but the adjacent buildSimctlArgsForAddress / buildSimctlArgsForDevice return plain string[] — an unexplained asymmetry between functions whose names differ by one word. The consequence is that the brand lands on the minority host.appleTools.run path, while the dominant build* + runSimctlForDevice / runXcrun path — and the cross-package façade @agent-device/platform-apple/simctl — hand out unbranded string[]. So R79, not the compiler, is what actually enforces the invariant on most call sites. If you brand the build* return (a branded readonly string[] is still assignable to the readonly string[] / string[] sinks those ~20 call sites use, so they compile untouched), the guarantee spans both execution paths and the ArrayExpression + '--set' clauses of R79 become deletable — its own kill criterion says exactly this. Even if you keep the current scope deliberately, this naming asymmetry should at least be explained in a comment.
| ) { | ||
| report(node, ARGV_MESSAGE); | ||
| } | ||
| if (file.startsWith(APPLE_SRC) && file !== SIMCTL_OWNER && isStringLiteral(node, '--set')) { |
There was a problem hiding this comment.
Two fragility problems with how the gate encodes the boundary. (a) This line rejects the string literal '--set' anywhere under the apple src tree except the simctl owner — an unrelated xcrun flag, an args.includes('--set') diagnostic, or a user-facing message would fail CI with "use scopeSimctlArgsForDevice"; and it is trivially evaded (a `--set` template literal, '--' + 'set', a const FLAG). That is a repo-wide string ban, not "a simctl argv built by hand". AGENTS.md is explicit about keying on typed structure, never text — this clause is precisely the text-sniffing kind. (b) The owner model is hardcoded path sets (ARGV_OWNERS, SET_SCOPE_OWNERS); every new device-less simctl call has to edit this allowlist, so the boundary accretes by path rather than by construction. Both mostly dissolve if the type from the finding above carries the invariant — the strongest clause should be "a simctl argv not produced by the branded builder", asserted on the call node, not a global '--set' match. Worth narrowing now even if you keep the gate.
|
Summary
Makes an unscoped
simctlargv a type error at the Apple tool port, closing the gap behind the--set-drop bugs fixed by #2818 and #2824.AppleToolRequestbrandssimctlargv asScopedSimctlArgs; raw argv no longer compiles onhost.appleTools.run.core/simctl.tsadds aSimulatorAddressbrand (simulatorAddressFor(device)) and requires anexplicit set scope on
scopeSimctlArgs;buildSimctlArgsis replaced by address/device builders.AppleToolProvider.simctl.runtakes the branded argv.log-predicate.tsandsnapshot-source/host.tstake the set from the device'sSimulatorAddress.R79 apple-simulator-scopefails CI on a hand-builtsimctlarray, a forged brandcast, a stray
--setliteral, orscopeSimctlArgsoutside its three owners.No argv output changes. 36 files touched, +609/-136 gross, net +22 production lines.
Closes #2878
Validation
Tested at
2cee7ed3eb(baseorigin/main0749551b71).pnpm typecheck,check:layering(zero R79 violations),check:fallow,check:production-exports: pass.check:affected --run: run 1 had one unrelated timeout (screenshot-density.test.ts, alsoreproduces pre-branch under equal load); run 2 passed, 457 files / 3283 tests.
open/snapshot/logsproven end to end, argv unchanged. Scoped-setbridge-spawn and log-stream argv proven live with
--set, bridge answered from inside the set.A full clean scoped snapshot wasn't reached: an existing, unrelated defect deletes the scoped
simulator during runner fallback under high host load (evidence kept for follow-up).