fix(scope)!: resolve --computer/--mobile-device to an ID and send <id> alone - #409
Conversation
…> alone A Mac's UDID never reached the wire as a UDID. udidRe matched only the 40-hex shape older iOS devices use, so a UUID-shaped Mac UDID went out as <name>; and once matched, NamedItem's untagged Name still sent an empty <name></name> beside the <udid>. The Classic computer matcher reads <id>, <name>, <udid> in order and refuses at the first one present that does not match, so both forms answered 409 "Unable to match computer" on every computer-scoped resource. The mobile-device matcher accepts any matching element, which is why mobile UDIDs worked. Wire-checked 2026-10-01 on platform-nmjspp: each of <id>, <name>, <udid> and <serial_number> resolves alone, case-insensitively; a stale <id> or an empty <name> ahead of a good identifier fails. Names are not unique and the server's name match picks one duplicate silently. --computer and --mobile-device now take an ID, name, UDID or serial number, resolved in one inventory request (/v4/computers-inventory, /v2/mobile-devices/detail, both published on the gateway) to the device's ID, which is the only identifier sent. add's duplicate check, remove and the read-back verification compare IDs, so remove by serial number now works (the scope GET carries no serial). remove falls back to the literal value only when nothing matches, so a member whose inventory record is gone can still be taken out. Also: parseMobileDevice reads general.udid and hardware.serialNumber, which is where /v2/mobile-devices/detail puts them. BREAKING CHANGE: a device name shared by more than one device is refused instead of letting the server pick one; pass the ID. The resolution needs Read Computers or Read Mobile Devices on the API client. Co-Authored-By: Claude Opus 5.5 <[email protected]>
ktn-jamf
left a comment
There was a problem hiding this comment.
Warning
--computer / --mobile-device to a numeric ID before any scope write and sends <id> alone. The design is sound and well verified on the wire.
Blocking: (1). Two nice-to-haves sit in the collapsed section.
Rating: 3/5
- Would be a 5 with the CHANGELOG entry in (1).
- Coverage: no specialist lane ran (reviewed inline); the rating reflects the dimensions I searched, not a lane panel.
Findings
CHANGELOG.md: a self-declared breaking change adds no entry
The title is fix(scope)!: and the body has a Breaking section (shared device names refused, new Read Computers / Read Mobile Devices requirement). CHANGELOG.md opens by saying it exists to record breaking changes and "what the migration is", and the other recent ! PRs (#407, fe2a3e6f, f012e1c5) each added an entry under ## Unreleased. This diff touches none.
Failure scenario: a script that runs scope add --computer <name> with an API client lacking Read Computers, or on a name shared by two devices, starts failing after upgrade, and the only place that explains it is the PR body, which release notes do not carry.
Suggested fix: add a ### Breaking — ... entry under ## Unreleased covering the two behaviour changes, the migration (pass the numeric ID; grant Read Computers / Read Mobile Devices), and the fact that remove by serial number now works.
Fixed when: CHANGELOG.md carries the entry in this PR.
Nice-to-have suggestions (2 items)
💡 NICE-TO-HAVE (2) (correctness) — internal/scope/scope.go:345, :463, internal/scope/verify.go:114: after resolution name is a numeric ID, but AddToScope, removeNamedItem and itemPresent still also match it against item.Name / item.UDID. A different member whose name is the same digits (a device renamed 107) makes add report "already in scope" and remove delete both. The PR text says these now "compare IDs"; for a resolved device they could compare item.ID only. Promote to IMPORTANT when: device names that are all digits occur in a tenant using this command.
💡 NICE-TO-HAVE (3) (usability) — internal/resolve/identifier.go:103: a failed lookup (for example the 403 when the client lacks Read Computers) surfaces as looking up computer "5": HTTP 403. The new requirement is documented in --help, but the error does not name the privilege, and a plain numeric ID that worked before now fails the same way. A hint on 401/403 naming Read Computers / Read Mobile Devices would make the breaking change self-explaining.
This covers all findings — addressing (1) gets this PR to merge-ready.
Review coverage and scope
- Correctness: resolver OR-filter, numeric-ID precedence, wildcard re-check, ambiguity refusal, remove fallback,
<id>-only body (NamedItem.Nameomitempty). Finding (2) - Test coverage: resolver and target-resolution tests present;
go test ./internal/scope/ ./internal/resolve/passes at the reviewed head (run withJAMF_URL=https://nonexistent.invalid). - Conventions: CHANGELOG policy, finding (1). Output routing, flag rules and generated-code boundary not touched.
- Security: no credential handling; user value is RSQL-escaped (
EscapeRSQL) and URL-escaped. - [na] Performance (one extra GET per device flag), frontend, migrations, dependencies.
Diff: 8 files, +559/−25, head 14fbe40. Lanes run: none, reviewed inline; the gateway and wire claims in the PR body (platform-nmjspp, 128 operations) were not re-run against a tenant. Never run: all specialist lanes. Conventions: root CLAUDE.md and the three imported rules files, read from the PR worktree.
🤖 Generated by the pr-review:review skill · reviewed head 14fbe40
…ivilege on a 403 A resolved --computer/--mobile-device carries the device's numeric ID as its name, and add, remove and the verification read also compared it against each member's name and UDID. A different device whose name is the same digits then counted as already in scope on add, was removed alongside it on remove, and stood in for it in the read-back. A resolved target now matches item.ID only; an unresolved literal (remove's fallback) still matches by name, ID or UDID. A 401/403 on the inventory lookup now keeps its exit code and adds a hint naming Read Computers / Read Mobile Devices, since the lookup is the new requirement and a numeric ID that worked before now fails there. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… change Co-Authored-By: Claude Opus 5.5 <[email protected]>
|
Thanks. All three are addressed in (1) CHANGELOG. There is now a
(2) Resolved ID matched against names. A resolved target now matches
An unresolved literal (remove's no-match fallback) still matches by name, ID or UDID. Tests cover the helpers and both commands, with a computer 31 named (3) 403 hint. A 401 or 403 on the inventory lookup keeps its exit code. It adds a hint naming Read Computers or Read Mobile Devices, and keeps any hint the client already attached. A 5xx gets no privilege hint. Mutation-checked. Wire, platform-nmjspp, on a disposable policy (5809, since deleted):
The scope ended empty, as it started. |
ktn-jamf
left a comment
There was a problem hiding this comment.
Tip
✅ Merge-ready. All three round-1 findings are fixed in 7b6edb98 and 24fe0f99. Resolved devices now match by ID alone, a 401/403 on the lookup names the missing privilege, and the CHANGELOG carries the breaking entry.
Rating: 5/5
- Every prior finding is fixed in code; nothing new found in the incremental diff.
- Coverage: no specialist lane ran (reviewed inline); the rating reflects the dimensions I searched, not a lane panel.
Prior findings status
| # | Location | Rounds | State | Notes |
|---|---|---|---|---|
| (1) | CHANGELOG.md |
1 | ✅ Fixed | ### Breaking — scope add/remove resolve --computer and --mobile-device to an ID first under ## Unreleased: shared-name refusal, Read Computers / Read Mobile Devices requirement, remove by serial, migration. The "exit 5" claim matches exitcode.PermissionDenied. |
| (2) | internal/scope/scope.go, verify.go |
1 | ✅ Fixed | ScopeTarget.matches compares item.ID only when Device is set. Used by AddTargetToScope, RemoveTargetFromScope, targetPresent and DiffScope. The literal remove fallback still matches name, ID or UDID. |
| (3) | internal/resolve/identifier.go |
1 | ✅ Fixed | lookupError adds a hint naming readPrivilege on exit codes 3 and 5, keeps any hint the client attached, and leaves other errors unwrapped. |
Review coverage and scope
- Correctness: the new matcher at all four call sites; no remaining caller of the old name-matching path for a resolved device.
- Test coverage:
go test ./internal/scope/ ./internal/resolve/passes at the reviewed head (run withJAMF_URL=https://nonexistent.invalid). The PR reports a computer named107beside computer 107, and four mutation checks. - Conventions: CHANGELOG policy satisfied.
- [na] Security, performance, frontend, migrations, dependencies: unchanged since round 1.
Diff since 14fbe40: the PR's own changes are CHANGELOG.md, internal/resolve/identifier.go, internal/scope/{commands,scope,types,verify}.go and tests. The rest of the range is the main merge (#402, MCP run_command), which is not part of this PR's change. Lanes run: none. Never run: all specialist lanes. The wire results the author posted (platform-nmjspp) were not re-run; no tenant was called.
🤖 Generated by the pr-review:review skill · reviewed head 24fe0f9
# Conflicts: # CHANGELOG.md
Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts: # CHANGELOG.md
Problem
scope add/remove --computer <UDID>answered409 Unable to match computeron every computer-scoped resource, although--helpadvertised UDIDs. There were two defects:udidRematched only the 40-hex UDID older iOS devices use, so a Mac's UUID-shaped UDID was sent as<name>.NamedItem.Namehad noomitempty, so the body carried<name></name><udid>…</udid>.Wire behaviour (platform-nmjspp, 2026-10-01, raw PUTs, targets and exclusions)
<id>,<name>,<udid>and<serial_number>each resolve on their own, case-insensitively.<name>, an empty or stale<id>, orid=0ahead of a good<udid>all answer 409. A good<id>wins over a bad name or UDID.<id>alone is the only form that is unambiguous and resolves on both matchers.Fix
resolve.ResolveComputerIdentifier/ResolveMobileDeviceIdentifier: one inventory request that ORs id (when numeric), name, UDID and serial number. Endpoints are/v4/computers-inventoryand/v2/mobile-devices/detail, both published on the gateway.==treats*as a wildcard.NoDeviceMatchError.--computer/--mobile-devicefirst and send<id>alone.removefalls back to the literal value only on no-match, so a member whose inventory record is gone can still be removed.Added computer "fwwt058jg5" (id 107, ARMADA-058JG5) to exclusion scope of id 5717.parseMobileDevicenow readsgeneral.udidandhardware.serialNumber, which is where/detailputs them.Breaking
Verification
<id>only, and every scope ended byte-identical to where it started. Onmain, the same matrix failed every computer UDID add with a 409./detailshape) and for target resolution (<id>-only body, remove by serial, remove fallback, non-device flags untouched). Mutation-checked.go test ./...passes.🤖 Generated with Claude Code