Repository navigation
fleet: client roster row carries friendly name + device type (shared schema derivation) #1371
Description
Important
Problem — The client enroll POSTs a roster row with name = hostname() (raw FQDN) and no device_type, so peers see an ugly, typeless row while the machine labels itself nicely locally. See #1368 for the full symmetric problem and approaches considered.
Approach — Move the name+type derivation into a form shared by the enroll producer and the extension self-row so they cannot disagree: pure classifiers in @amicode/schema, an impure command-runner seam per node package. Populate the client's POSTed row via the resolution precedence.
Scope — in: the pure schema functions; the per-package impure detector behind one injectable command-runner seam; the client enroll row carrying friendly name + device_type; the amicode.device.name / amicode.device.type override namespace unified across the enroll producer AND the extension self-row; rehoming the extension's classifyDeviceType to schema. · out: the server self-registration and the server/self-row de-dupe (that is the sibling slice); the roster JSON schema (fields already exist).
Assumptions — Detection is best-effort; an undefined type falls back honestly to server_mode. The enroll verb already reads settings.json (for amicode.fleetTransport), so reading the override keys reuses that path.
Acceptance Criteria
-
@amicode/schemaexports pureclassifyMacModel(modelName)→laptop/desktop/undefined(rehomed from the extension'sclassifyDeviceType; existing behavior preserved). -
@amicode/schemaexports pureclassifyLinuxChassis(chassis)→laptop(laptop/notebook/portable),desktop(desktop/tower),server(server/rack), elseundefined. -
@amicode/schemaexports purenormalizeDeviceName(raw)proven by a table:Mac.mynetworksettings.com → Mac,host.local → host,JJ's Mac Studio → JJ's Mac Studio,"" → "". Rule: strip from the first.onward only when the remainder is a DNS-suffix-shaped label sequence; leave dot-free / space-bearing names intact. -
@amicode/schemaexports pureisWslKernel(procVersion)→ true on a Microsoft/WSL marker, extracted from the currently-inline regex in the extension'sdetectWSLVersion. - The extension self-row imports
classifyDeviceTypefrom@amicode/schema(behavioral) and a lint/grep gate confirms no private classifier body remains in the extension (structural). - Each node package (extension host +
amico-run) exposes a single injectable command-runner seam; a per-package test feeds canned command output through the real caller into the shared pure classifier (so platform behavior is failing-test-able without shelling out). - Linux type source order is
hostnamectlchassis →/sys/class/dmi/id/chassis_type→ abstain;hostnamectlabsent ⇒ prettified hostname +device_typeomitted. - A client enroll POSTs a roster row whose
nameis the resolved friendly name and whosedevice_typeis the resolved type. - Resolution precedence per field, exercised by injected seams (
detectDeviceType?, a setting reader): explicit setting (amicode.device.name/amicode.device.type) → OS detection → prettified hostname (name) /undefined(type). Override-wins and honest-fallback are tested independently. - The self-row and the POSTed row resolve the same override string (both read
amicode.device.name/amicode.device.type); theamicode.device.namesetting description is updated to reflect it also names the fleet device. - Wiring a friendly
namedoes NOT changecanonical.hostor the minted join token (the friendly-name seam is decoupled from the canonical-host derivation).
Testing Decisions
- Extend
packages/schema's roster tests with a table-driven suite for the four pure functions (Mac model strings, Linux chassis values,/proc/versionWSL strings, thenormalizeDeviceNametable). - Extend
packages/amico-run's enroll tests (injected seams): the POSTed client row'sname/device_type, override-wins, honest-fallback, and the friendly-name-does-not-touch-canonical case. - Add a per-package impure-detector test feeding canned command output through the real command-runner seam.
- Reuse-first: extend the existing extension suite covering
readLocalDevice/classifyDeviceTypefor the import + override-parity assertions; no new file.
Key Decisions
- Pure classification lives in
@amicode/schemabesideKNOWN_DEVICE_TYPES; the producer and the extension host self-row both call it. The browser sidebar module remains a third re-declaring site and cannot import schema (schema pulls node builtins). - The impure
execSync/file-read platform branch hides behind one injectable command-runner seam per package so both copies are test-covered against canned output. - One override namespace read by both producers:
amicode.device.name(existing) + newamicode.device.type. - The friendly
namenever feedscanonical.host/ the join token.
Constraints & Invariants
@amicode/schemastays side-effect-free (only pure functions added; nochild_process).- No silent fallback: undetected type →
server_modefallback; no friendly-name source → prettified hostname; nothing fabricated. - Roster JSON schema unchanged (
name/device_type?already exist and validate). - Never-fork (ADR 0005): no engine-spawn path added.
Prior Art
- The roster contract + vocabulary (
RosterRow,KNOWN_DEVICE_TYPES,parseRosterRow) in the schema package. - The client enroll path + injectable seams +
settings.jsonreader (defaultSetTransport/defaultSettingsPath) inamico-run. - The existing derivation (
scutilComputerName,system_profilerclassifier,readLocalDevicereadingamicode.device.name) in the extension; the proven WSL/proc/versionregex in the rebuild host-matrix code.
Activity
fleet: self-reported device identity in the roster (#1368) (#1374)
jeonghun-jj-lee commentedon Sep 21, 2026
Merged into feature/free-tier-fleet via #1374 (merge commit 1b2a3df). Auto-close was skipped because the PR's base was a feature branch, not the default branch — closing as merge bookkeeping.
fleet: self-reported device identity in the roster (#1368) (#1374)