The lean-build job proved the delete-a-plugin guarantee with two plugins and four hand-picked marker components. It now builds a real twelve-plugin set, and the assertions follow a rule instead of a hunch: one component from EVERY omitted plugin must be absent, one from EVERY chosen plugin must be present, and the backend tree is checked both ways. An omitted set that grows without its check growing is how a leak ships. CHANGELOG had not been touched across the whole range, so the largest security-behaviour change in the product - per-PC endpoint credentials, the shared-key refusal for enrolled PCs, server pinning, agent 1.20.1 and tray 1.7.1 - had no release record. An [Unreleased] section now carries it, written from the commits rather than from memory. CLAUDE.md and the agent quick-reference stated that ADR-016 bans storing credentials in the registry. It does not, and this fleet depends on HKLM for its collector key and server pins: the next person following that text removes them and silently breaks reporting and pinning on every un-enrolled PC. Corrected, along with two other statements that were false on this branch - the database is MySQL, not PostgreSQL, and column names are lowercase concatenated, not snake_case, which is the opposite of what the gate enforces. The wiki drafts are updated against the code as it stands, each old finding given a verdict with a file and line rather than left as an assertion from a week ago, and a new page records the ADR conformance result - including that the ADR set itself is not on this branch, so nothing here enforces it except the naming and style script. They are also scrubbed of the things that must not reach a published wiki: the site name, the internal git server, developer home paths and the assistant tooling. They now pass the repository's own publishability patterns, which is what the scrub is for.
557 lines
31 KiB
Markdown
557 lines
31 KiB
Markdown
# ADR Conformance Review - September 18, 2026
|
|
|
|
**Branch:** gh-wj at 35d4fcd41
|
|
**Range reviewed:** 2ac2e6d33..gh-wj, 20 commits, 149 files, +22055 lines
|
|
**Method:** read-only. Every statement below cites a file and line that was read.
|
|
No test suite was run except `tests/test_plugins/test_kiosk_network_allowlist.py`
|
|
(20 passed) and `bash scripts/check-naming-and-style.sh` (exit 0).
|
|
|
|
## Read this first: the ADRs are not in this repository
|
|
|
|
`ls docs/adr` fails on gh-wj. There is no ADR directory on this branch. The
|
|
whole set, ADR-001 through ADR-018 plus a README, lives on the other lineage at
|
|
`docs/adr/ in the product repo` and was read there for this review.
|
|
`docs/` on gh-wj holds only `ShopDB-Planner-Tasks-v2.csv`, `deployment/`,
|
|
`notes/`, `openapi.json` and `wiki-drafts/`.
|
|
|
|
Three consequences, and they are the reason this page exists:
|
|
|
|
1. **Nothing on this branch states the rules.** A person working here has no
|
|
ADR to read, so a decision can be contradicted without anyone noticing that
|
|
a decision existed. That is exactly what happened to ADR-016 (see below).
|
|
2. **Nothing on this branch enforces them except one script.**
|
|
`scripts/check-naming-and-style.sh` is the only ADR-aware gate in the repo,
|
|
and it names two ADRs: ADR-017 at line 343 and ADR-015 at line 395. Its other
|
|
eight checks enforce CONTRIBUTING.md house rules, not ADRs. A handful of
|
|
tests cover ADR ground without being described that way:
|
|
`tests/test_plugin_migrations.py` (ADR-008 chain shape and ownership),
|
|
`tests/test_core/test_plugin_settings_hubs.py` (ADR-019),
|
|
`tests/test_plugin_contract.py` (manifest fields),
|
|
`tests/test_core/test_public_read_surface.py` (the read gate).
|
|
`tests/test_api_inventory_parity.py` skips itself entirely on this branch:
|
|
`docs/api-inventory.json` is not tracked here, and its own skip reason says
|
|
"docs/ is excluded from publication".
|
|
3. **Some violations cannot be fixed from here.** ADR-018 has no
|
|
implementation-status note; ADR-016 is still marked "decided, NOT yet
|
|
implemented". Both corrections belong in files that are not in this
|
|
repository.
|
|
|
|
The one ADR-shaped file on gh-wj is the ADR-019 draft in this folder, and it is
|
|
a draft.
|
|
|
|
## Result by ADR
|
|
|
|
| ADR | Subject | Result |
|
|
|-----|---------|--------|
|
|
| ADR-001 | Asset as platform contract | Two exceptions, both below: the unbumped Asset cert columns (2) and raw SQL over core tables (6) |
|
|
| ADR-002 | Plugin versioning | Holds for the bumps. Two stale manifests, and one unbumped core-model change (2) |
|
|
| ADR-003 | Plugin distribution | Holds |
|
|
| ADR-004 | Deployment topology | Holds |
|
|
| ADR-005 | Equipment vs measuringtools | Superseded by ADR-018 |
|
|
| ADR-006 | Collector contract | Holds on shape. One gap in a new boundary |
|
|
| ADR-007 | Product versioning and releases | VIOLATED (CHANGELOG) |
|
|
| ADR-008 | Plugin migration ownership | VIOLATED (plugin chain, core table) |
|
|
| ADR-009 | Frontend plugin gating | Holds |
|
|
| ADR-010 | Frontend plugin hooks | Holds |
|
|
| ADR-011 | Machines rename | Holds |
|
|
| ADR-012 | GE-Enforce manifest ownership | Holds |
|
|
| ADR-013 | Plugin catalog and lean builds | Holds |
|
|
| ADR-014 | Schema-lean per site | Holds |
|
|
| ADR-015 | Site-specific configuration | Holds in scope. Scope has a hole |
|
|
| ADR-016 | Credential delivery | Unbuilt by its own status, and MISDESCRIBED in four files here |
|
|
| ADR-017 | Buildings and levels | Holds |
|
|
| ADR-018 | Metrology instruments are machines | Accepted, not implemented |
|
|
| ADR-019 | Plugin dynamic settings routes (draft) | Implementation matches the draft |
|
|
|
|
## What holds, and the evidence
|
|
|
|
**ADR-001, the import surface.**
|
|
`grep -rn "from shopdb\.\(core\|utils\|extensions\|services\)" plugins/
|
|
--include=*.py` returns zero hits. The only `shopdb.*` import in `plugins/`
|
|
outside `shopdb.api` is `shopdb.plugins.alembic_template` from migration
|
|
`env.py` files, which is the shared Alembic runner ADR-008 sanctions. The three
|
|
names added to the contract this range - `BusinessUnit`, `ip_in_cidrs`,
|
|
`request_ip_allowlisted` - are both defined and listed in `__all__`
|
|
(`shopdb/api/__init__.py:190-194`, `:471-486`). The new endpoints tables key off
|
|
`assets.assetid` by foreign key rather than introducing a parallel identity.
|
|
This covers the names added and kept. It does NOT cover a name added to the
|
|
Asset model and then withdrawn again without a bump, which happened in this
|
|
range; see finding 2.
|
|
|
|
**ADR-002, the bumps.** `__contract_version__` moved 0.32.0 to 0.35.0 with a
|
|
per-bump rationale naming what was added and why it is additive
|
|
(`shopdb/__init__.py:124-152`). Every name those comments claim is genuinely
|
|
exported. Every new `BasePlugin` hook is optional with a safe default
|
|
(`shopdb/plugins/base.py:313-332`), so minor is the right classification. The
|
|
loader still checks `core_version` against `__contract_version__`
|
|
(`shopdb/plugins/loader.py:255-258`).
|
|
|
|
**ADR-004 and ADR-014, per-site instances.** No `siteid` column anywhere in the
|
|
new schema. utf8mb4 still pinned in `.env.example`. The two-command deploy
|
|
(`flask db upgrade` then `flask plugin upgrade-all`) is unchanged. All four new
|
|
endpoints tables are registered in `PLUGIN_TABLE_OWNERS`
|
|
(`shopdb/plugins/alembic_template.py:57-63`) in the same change that creates
|
|
them, so `flask plugin prune-schema` still leaves a lean site lean.
|
|
|
|
**ADR-006, the collector shape.** `/api/collector/<pluginname>` is still the
|
|
single dynamic dispatch route (`shopdb/core/api/collector.py:229`), the response
|
|
envelope is byte-for-byte the ADR's `{status, action, assetid, identityvalue,
|
|
warnings}` (`:324-331`), every submission still writes an AuditLog row
|
|
(`:313-320`), `/api/collector/_schemas` is still JWT-protected (`:337-341`), and
|
|
per-plugin `COLLECTOR_API_KEY_<PLUGIN>` with shared fallback resolves as the ADR
|
|
specifies (`:194-203`). Identity integrity under the new credential mode is
|
|
enforced in all three schema-declaring plugins - a per-PC credential reports
|
|
only for its own asset (`plugins/computers/plugin.py:729-733`,
|
|
`plugins/printers/plugin.py:556-559`, `plugins/endpoints/plugin.py:1422-1427`) -
|
|
and the dispatcher maps the `PermissionError` to 403 ASSET_MISMATCH
|
|
(`collector.py:294-299`).
|
|
|
|
**ADR-007, the version lock-step.** `__version__ = '0.12.0'`
|
|
(`shopdb/__init__.py:158`) matches `frontend/package.json:3`, and `__version__`
|
|
is still absent from `shopdb.api`.
|
|
|
|
**ADR-008 in the forward direction.**
|
|
`git diff --stat 2ac2e6d33..gh-wj -- migrations/` is empty. Not one line of
|
|
plugin schema went into the core chain. The endpoints chain is 24 revisions with
|
|
exactly one root and one head, no dangling `down_revision`, longest revision id
|
|
29 characters against the 32-character `alembic_version` cap.
|
|
|
|
**ADR-009, route gating.** Every plugin-owned route added or touched this range
|
|
carries `meta.plugin`, including the two new endpoints settings pages, the USB
|
|
kiosk and the printed-parts kiosk. `GET /api/plugins/enabled` stayed
|
|
`jwt_required(optional=True)` (`shopdb/core/api/plugins.py:31-42`) through the
|
|
read-permission sweep that gated 165 other GETs, so the unauthenticated kiosk
|
|
gate still works.
|
|
|
|
**ADR-010 and ADR-013, frontend boundaries.** The new
|
|
`frontend/src/components/NetworkAllowlistPicker.vue` is a CORE component
|
|
consumed by plugin views through the `@/` alias
|
|
(`plugins/usb/frontend/views/USBSettings.vue:108`,
|
|
`plugins/printedparts/frontend/views/PrintedPartsSettings.vue:164`), which is
|
|
the established pattern; 97 plugin frontend files already import that way. No
|
|
core file gained a hardcoded plugin import.
|
|
|
|
**ADR-012, GE-Enforce ownership.** The per-PC rollout keeps ShopDB as author of
|
|
record: the runbook's JSON snippets are entries to author and publish through
|
|
the plugin, not hand edits to the share, and `Set-ShopDBServerPins.ps1` is
|
|
staged as a normal PS1 entry with `TargetHostnames`/`TargetMachineNumbers`
|
|
gates. The engine is still referenced, not vendored.
|
|
|
|
**ADR-015 within its enforced scope.** `scripts/check-naming-and-style.sh` exits
|
|
0 and the pipeline is not vacuous: a control pattern over the same six
|
|
directories returns 1995 lines. The new kiosk network admission is a setting
|
|
with a blank default and a documented-neutral example
|
|
(`plugins/usb/plugin.py:153-168`), blank means off
|
|
(`shopdb/utils/ipallowlist.py:44-46`), and `frontend/vite.config.js:5-16`
|
|
replaced a hardcoded dev-proxy address with `VITE_DEV_API_TARGET`.
|
|
|
|
**ADR-017.** The per-occurrence `mapx`/`levelid` gate and the
|
|
`LocationMapTooltip :levelid` gate both pass. No payload in this range emits a
|
|
position without its level.
|
|
|
|
**ADR-019 against the implementation.** `SETTINGS_HUBS_WITH_PLUGIN_TABS =
|
|
('model-catalog',)` at `shopdb/plugins/base.py:29`; the loud drop at `:32-54`;
|
|
the `settings.view` gate at `shopdb/core/api/plugins.py:47`; the still
|
|
unrewritable `` import(`../../${tab.component}`) `` at
|
|
`frontend/src/views/settings/ModelCatalogHub.vue:84` with no `vite.config.js`
|
|
change to fix it; and a non-vacuous test suite in
|
|
`tests/test_core/test_plugin_settings_hubs.py`. The draft in this folder was
|
|
corrected on 2026-09-18: its code block still showed
|
|
`@jwt_required(optional=True)` and its Future Extensions section still called
|
|
the endpoint anonymous, both contradicting its own Auth paragraph and the
|
|
shipped code.
|
|
|
|
## Violations
|
|
|
|
### 1. ADR-007: no release record for the largest change in the range
|
|
|
|
`CHANGELOG.md:11`
|
|
|
|
`git diff --stat 2ac2e6d33..gh-wj -- CHANGELOG.md` returns nothing at all. The
|
|
file is byte-identical across all 20 commits, which include per-PC agent
|
|
credentials, enrollment, bootstrap tokens, server pinning, kiosk network
|
|
admission and three contract bumps. Grepping the whole file for "per-PC",
|
|
"X-Endpoint-Credential", "Endpoint credential" and "enrol" finds nothing
|
|
relevant. ADR-007 decision point 3 makes `CHANGELOG.md` the canonical
|
|
human-readable record and says work in flight accumulates under
|
|
`## [Unreleased]`; that block still describes only the pre-range share-link and
|
|
read-gate work.
|
|
|
|
Low severity: no release has been cut in the range, so the obligation point
|
|
(ADR-007 release procedure step 3) has not been reached and the debt is fully
|
|
repairable before the tag. Operator-facing content does exist, at
|
|
`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md`, but that is plugin-local
|
|
and not where ADR-007 says an adopter looks.
|
|
|
|
### 2. ADR-008: a plugin migration chain does DDL on the core `assets` table
|
|
|
|
`plugins/endpoints/migrations/versions/0023_endpoint_credentials.py:73` and `:76`
|
|
|
|
0023 declares `plugin_name = 'endpoints'` (line 33) and then runs
|
|
`op.drop_index(index['name'], table_name='assets')` and
|
|
`op.drop_column('assets', column)`. It is cleaning up after itself:
|
|
`git show 5cd533239:plugins/endpoints/migrations/versions/0021_client_certificates.py`
|
|
shows the shipped 0021 adding three columns to `assets` plus
|
|
`idx_assets_cert_fingerprint` from the same plugin chain. `assets` is not, and
|
|
cannot be, in `PLUGIN_TABLE_OWNERS['endpoints']`
|
|
(`shopdb/plugins/alembic_template.py:57-63`), so the metadata ownership filter
|
|
never saw it, and `tests/test_plugin_migrations.py` has no test over which
|
|
tables a chain touches.
|
|
|
|
Low severity: every step is guarded on the live schema (0023:64-76), 0021 and
|
|
0022 are now no-ops, the forward direction is clean, and
|
|
`tests/test_plugins/test_endpoints_credentials_migration.py:96-140` runs the
|
|
chain twice from both a legacy and a fresh schema. The lane crossing is real;
|
|
the fleet blast radius is essentially zero.
|
|
|
|
**The ADR-001/ADR-002 half of this was written up wrong, and that conclusion is
|
|
withdrawn here (2026-09-18).** The original text read: "`git show 5cd533239
|
|
--stat` touched no file under `shopdb/`, and `grep -rn cert
|
|
shopdb/core/models/asset.py` is empty: the Asset *model* never carried those
|
|
columns. Only the database did. No contract bump was owed." Both halves of the
|
|
evidence are real and the conclusion drawn from them is still false, because
|
|
5cd533239 is not the only commit in the range and `grep` only sees the tree as
|
|
it stands now. `git show 18e5fdc00:shopdb/core/models/asset.py` puts
|
|
`client_cert_fingerprint` (declared `index=True`), `cert_issued_at`,
|
|
`cert_expires_at`, `cert_revoked_at` and `cert_revocation_reason` on the CORE
|
|
`Asset` model. 18e5fdc00 (2026-09-14, "Fix critical security vulnerabilities in
|
|
client certificate authentication") is an ancestor of `gh-wj`; 7480e4fe3
|
|
(2026-09-15) deleted all 28 lines again. The grep comes back empty because it
|
|
runs after the retraction, not because the contract was never touched.
|
|
|
|
The ADR-001/ADR-002 finding therefore holds, in a worse form than first stated:
|
|
the core Asset contract surface was extended to carry a plugin's feature and
|
|
then retracted, with no contract bump in either direction, and for the length of
|
|
that window five underscore-spelled columns sat on a core model in a repo whose
|
|
column convention forbids underscores. Severity stays low - the window is inside
|
|
an unreleased branch, and nothing outside the endpoints plugin ever read the
|
|
columns - but the finding is a real ADR-001/ADR-002 breach and not, as written,
|
|
a non-finding.
|
|
|
|
**Now guarded, 2026-09-18.** The finding above ended with "no test over which
|
|
tables a chain touches". There is one:
|
|
`tests/test_plugin_migrations.py::test_no_plugin_migration_touches_another_lanes_table`.
|
|
It AST-scans every `plugins/*/migrations/versions/*.py` for `create_table`,
|
|
`drop_table`, `add_column`, `drop_column`, `alter_column`, `create_index`,
|
|
`drop_index`, `rename_table`, the five constraint ops (`create_foreign_key`,
|
|
`drop_constraint`, `create_unique_constraint`, `create_check_constraint`,
|
|
`create_primary_key`) and the `batch_alter_table` forms, resolves each target
|
|
table through module-level constants and literal `for` sequences, and fails when
|
|
the table belongs to `core` or to a different plugin. Ownership comes from
|
|
`PLUGIN_TABLE_OWNERS` and a static scan of `shopdb/core/models/` (including the
|
|
three bare `db.Table` association tables), so the map stays the single source of
|
|
truth rather than a table list copied into a test.
|
|
|
|
Raw SQL is scanned too, because `op.execute('ALTER TABLE assets DROP COLUMN
|
|
assetnumber')` is DDL that no `op.*` scan can see. `op.execute` and
|
|
`exec_driver_sql` have their statement text flattened - f-string holes and
|
|
unresolvable names become a sentinel character rather than being dropped - and
|
|
`ALTER TABLE x`, `DROP TABLE x`, `CREATE TABLE x`, `RENAME TABLE x TO y`,
|
|
`CREATE/DROP INDEX ... ON x` and `TRUNCATE x` name the op's target table.
|
|
`UPDATE`, `DELETE`, `INSERT` and `SELECT` are data, not schema, and are not
|
|
flagged: three shipped revisions already run cross-checked DML
|
|
(`plugins/backups/.../0002_backups_lastseenat.py:37`,
|
|
`plugins/backups/.../0003_backups_clear_backfilled_lastseen.py:58`,
|
|
`plugins/computers/.../0006_policyactionhold_indefinite.py:78`) and all three
|
|
stay green. Keeping the hole in place is what separates the two cases that
|
|
matter: `machines 0002` interpolates a CONSTRAINT name into
|
|
`f"ALTER TABLE equipment DROP FOREIGN KEY {name}"`, where the table is literal
|
|
text and resolves; `f'ALTER TABLE {t} DROP COLUMN x'` puts the hole inside the
|
|
table name, resolves to nothing, and fails as UNRESOLVED, which is the obvious
|
|
dodge closed.
|
|
|
|
Four supporting tests keep it honest: an op whose table name cannot be resolved
|
|
statically is a failure, not a pass, so a computed name does not slip through; a
|
|
non-vacuity test asserts the scan still finds more than 100 ops and still
|
|
detects the 0023 crossing; a second non-vacuity test pins two specific raw-SQL
|
|
ops out of `machines 0002` and asserts the three DML statements are absent; and
|
|
a dead-entry test fails if an allowlist line stops matching anything, so an
|
|
excuse cannot outlive what it excused.
|
|
|
|
The allowlist keys on `(plugin, filename, op, table, column)`, not on the first
|
|
four. Keyed on the first four alone, the one shipped `drop_column on assets`
|
|
entry would also excuse a brand new `op.drop_column('assets', 'assetnumber')`
|
|
appended to that same shipped file - the exact breach the guard exists to stop -
|
|
so the six entries name their columns: one `drop_index` (column `None`, the
|
|
index name is computed) and one `drop_column` per certificate column. 0021 and
|
|
0022 are not listed, because they are stamp-only no-ops and present no DDL to
|
|
scan.
|
|
|
|
Verified non-vacuous against the pre-fix version of the same test module. Three
|
|
violations were planted in a scratch copy of `plugins/` (the shipped 0023 is
|
|
applied on production and was not edited in place): the five constraint ops on
|
|
`assets` from a plugin chain, `op.execute` with both a literal `ALTER TABLE
|
|
assets` and an f-string table name, and a new `op.drop_column('assets',
|
|
'assetnumber')` inside 0023. The pre-fix guard passed all four of its tests on
|
|
all three plants. The current guard fails, naming all seven ops with file, line,
|
|
op, table, column and owner, and goes green again once the plants are removed.
|
|
|
|
**The 0023 index-drop loop was examined and needs no repair.** 0023:71-73 drops
|
|
any index on `assets` whose column set intersects the certificate columns and
|
|
recreates nothing, which would matter if such an index could carry columns that
|
|
still exist. It cannot. The complete set of things that create an index on
|
|
`assets` is:
|
|
|
|
* the core baseline `68b3947ae14f`: `idx_asset_active`, `idx_asset_location`,
|
|
`idx_asset_status`, `idx_asset_type_bu`, `ix_assets_assetnumber`,
|
|
`ix_assets_serialnumber`
|
|
* core `7b02`: `ix_assets_gaugelabreference`, `ix_assets_maintenancereference`
|
|
* core `7d33`: `idx_assets_levelid`
|
|
* the shipped endpoints `0021`: `idx_assets_cert_fingerprint` over
|
|
`client_cert_fingerprint` alone (the shipped `0022` added two columns and no
|
|
index)
|
|
* `db.create_all` off `shopdb/core/models/asset.py`. The earlier version of this
|
|
inventory said that builds "that same set" as the core baseline, on the
|
|
strength of the Asset model never carrying a cert column. As corrected above,
|
|
between 18e5fdc00 and 7480e4fe3 it did, with `client_cert_fingerprint`
|
|
declared `index=True`, so `create_all` in that window ALSO built
|
|
`ix_assets_client_cert_fingerprint`. That index is missing from every earlier
|
|
telling and is the one real correction to this list.
|
|
|
|
Re-checked rather than assumed, the conclusion is unchanged.
|
|
`ix_assets_client_cert_fingerprint` is single-column over
|
|
`client_cert_fingerprint`, which `_drop_certificate_columns` removes in the
|
|
statement immediately after the loop, so the index carries no column that
|
|
outlives it and there is nothing to recreate. The filter is also unreachable by
|
|
accident: every name in `_CERT_COLUMNS` is underscore-spelled, which the column
|
|
convention forbids, so no real `assets` column can collide with it. The only two
|
|
indexes the loop can ever drop are `idx_assets_cert_fingerprint` and
|
|
`ix_assets_client_cert_fingerprint`, both dead by construction. No index was
|
|
lost on any database at the reference site and no repair migration is owed; the reasoning is
|
|
recorded in the test file beside the allowlist entries so it is not
|
|
re-litigated.
|
|
|
|
### 3. ADR-016: four files here describe a decision ADR-016 never took
|
|
|
|
`docs/wiki-drafts/README.md:316-323` (corrected 2026-09-18), `CLAUDE.md:119`,
|
|
`the assistant quick-reference file, lines 135 and 14`,
|
|
`docs/wiki-drafts/Security-Audit-2026-09-11.md:392`
|
|
|
|
All four attribute "device tokens" to ADR-016. `grep -rni device
|
|
docs/adr/ in the product repo*.md` returns no hit for device tokens
|
|
in ANY ADR. ADR-016's Decision is a fleet credential store with Fernet at rest,
|
|
a targeting and precedence resolver reusing `plugins/geenforce/filters.py`, an
|
|
explain endpoint, `credentials.manage`, and `GET /api/collector/credentials`
|
|
behind a dedicated `credentials.fetch` scope.
|
|
|
|
The `docs/wiki-drafts/README.md` copy went further and, under a heading
|
|
"ADR-016: Credential Delivery", listed registry keys among the places
|
|
credentials must **never** live. ADR-016 says the opposite: its
|
|
"Alternatives rejected" section calls registry-only provisioning "a worthwhile
|
|
FIRST STEP" and "Recommended as tier one regardless". The ban also contradicted
|
|
shipped code on this branch:
|
|
`plugins/computers/client/Report-AssetToShopDB.ps1:367-374` reads `CollectorKey`
|
|
out of `HKLM:\SOFTWARE\GE\ShopDB`, and
|
|
`plugins/endpoints/client/Set-ShopDBServerPins.ps1:4-20` writes the server trust
|
|
pins into the same key by design, with
|
|
`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md:106-111` ("The registry
|
|
wins") making that the intended precedence. That section was rewritten on
|
|
2026-09-18; the three files outside `docs/wiki-drafts/` still carry the
|
|
misattribution and are out of scope for this edit.
|
|
|
|
Two qualifications, so nobody overcorrects: `PER-PC-CREDENTIALS-ROLLOUT.md:351-361`
|
|
deliberately plans to clear `CollectorKey` from both registry views at cutover,
|
|
so the shared key in HKLM is being retired anyway; and `ServerPins` are trust
|
|
anchors, not credentials.
|
|
|
|
### 4. ADR-016: the per-PC programme redefines "credential" with no design record
|
|
|
|
`plugins/endpoints/models/credential.py:1`
|
|
|
|
ADR-016's mechanism is unbuilt. Grepping `shopdb/` and `plugins/` for
|
|
`credentials.fetch`, `collector/credentials` and `Fernet` returns zero hits, and
|
|
`plugins/endpoints/plugin.py:154-265` declares only `endpoints.*` permissions -
|
|
no `credentials.manage`, no `credentials.fetch`. That matches ADR-016's own
|
|
"Implementation status, 2026-08-14". What the 20-commit programme built is a
|
|
different subject under the same word: `EndpointCredential` /
|
|
`EndpointEnrollRequest` / `EndpointMigrationWave` as the agent's own identity
|
|
secret (`models/credential.py:1-22`), `X-Endpoint-Credential` as the agent auth
|
|
header (`api/agentauth.py:1-18`), and an unauthenticated `/enroll`
|
|
(`api/enrollment.py:289`).
|
|
|
|
The design itself is sound and consistent with ADR-016's intent: the rollout
|
|
removes the cleartext key from the share, and rotation is a settings change plus
|
|
a pickup report, which is what ADR-016 wanted. The gap is that nobody wrote it
|
|
down as a decision. `grep -ril 'enroll|endpoint credential|X-Endpoint-Credential'`
|
|
over the ADR directory on the other lineage matches only ADR-012, which is
|
|
unrelated. Low severity and not fixable from here: the missing record belongs on
|
|
the lineage that carries `docs/adr/`.
|
|
|
|
### 5. ADR-018: accepted, unimplemented, with nothing saying so
|
|
|
|
`shopdb/plugins/alembic_template.py:74`
|
|
|
|
ADR-018 (ACCEPTED, 2026-09-04) decides that the `measuringtools` plugin is
|
|
retired and the `measuring_tool` asset type removed, that machines gains four
|
|
calibration columns and machinetypes gains `ismetrology`. None of it exists
|
|
here: `plugins/measuringtools/` still ships with its manifest,
|
|
`alembic_template.py:74` still registers
|
|
`'measuringtools': ('measuringtooltypes', 'measuringtools')`,
|
|
`shopdb/core/api/dashboard.py:19`, `:55` and `:93` still count `measuring_tool`
|
|
into the asset totals, `shopdb/cli/__init__.py:1021` still seeds it,
|
|
`deploy/site-profile-universal.json:12` still lists it in the universal profile,
|
|
and `grep -rn ismetrology shopdb/ plugins/` and
|
|
`grep -n calibration plugins/machines/models/*.py` both return nothing.
|
|
`git log -3 -- plugins/measuringtools/` shows no retirement work.
|
|
|
|
Unlike ADR-016, ADR-018 carries no implementation-status paragraph, so its text
|
|
reads as done. Low severity: the table holds zero rows, so nothing is broken
|
|
today, and ADR-018's own argument is that the window is time-boxed - the
|
|
migration is free while the 134 staged metrology instruments have not been
|
|
entered. gh-wj is the branch that deploys to the reference site, so entering them here is what
|
|
closes the window.
|
|
|
|
### 6. ADR-001 / ADR-002: raw SQL over core tables from a plugin
|
|
|
|
`plugins/computers/plugin.py:2080`
|
|
|
|
`db.session.execute(db.text(...))` joining core `assets`, `assetrelationships`
|
|
and `assettypes`, inside the `retype` CLI command. `Asset`, `AssetType`,
|
|
`AssetRelationship` and `RelationshipType` are all exported and in `__all__`
|
|
(`shopdb/api/__init__.py:47-48`, `:424`, `:438-439`), so unlike the
|
|
`BusinessUnit` case that the 0.34.0 bump note closed, there is no missing export
|
|
to blame, and `plugins/endpoints/api/routes.py:3981-3986` is the same pattern
|
|
already corrected. The 0.34.0 note names the reason in the code:
|
|
"a raw query is hidden coupling to the core schema that no contract bump can
|
|
catch, and it is the kind of thing that gets copied"
|
|
(`shopdb/__init__.py:135-142`).
|
|
|
|
Low severity and arguably generous: ADR-001's stated rule is an import rule,
|
|
which raw SQL does not literally breach, and the blast radius is one admin CLI
|
|
command with no serving route affected. Predates this range (331d27e07). The
|
|
other plugin raw query, `plugins/computers/plugin.py:2128`, touches only
|
|
plugin-owned tables and is fine.
|
|
|
|
### 7. ADR-015: the site-literal scan does not cover the C# agent
|
|
|
|
`scripts/check-naming-and-style.sh:440`
|
|
|
|
Lines 436-440 scope the grep to `plugins/ shopdb/ scripts/ deploy/
|
|
frontend/src/ tools/` with `--include` on
|
|
`.py/.vue/.js/.ps1/.psm1/.sh/.iss/.json/.html`. `agent/` is absent and `.cs` is
|
|
not an included extension. This range added roughly 2,500 lines of C# under
|
|
`agent/`, all of which ships to a sister site. Re-running the assembled site
|
|
patterns by hand over `agent/`, excluding bin and obj, finds one hit: prose in
|
|
`agent/ShopDbEndpointAgent/ShopDbEndpointAgent.csproj:108`, which ADR-015
|
|
explicitly permits ("Comments and documentation may name a site where it
|
|
explains WHY"). So the rule holds today; the gate would not notice if it stopped
|
|
holding.
|
|
|
|
**This is a coverage gap in the rule, not a breach of it.** ADR-015's 2026-08-14
|
|
amendment names those six directories as the scan's scope, so the script matches
|
|
the ADR precisely. The amendment widened the scan on exactly this reasoning -
|
|
the original scope missed every literal that reached a second site, because
|
|
those were in the client scripts and the installer - and the C# agent is now
|
|
that client channel.
|
|
|
|
### 8. ADR-002: two manifests declare a floor below the contract name they import
|
|
|
|
`plugins/usb/manifest.json:8` and `plugins/printedparts/manifest.json:8`
|
|
|
|
`da91a2915` added `shopdb/utils/ipallowlist.py`, bumped `__contract_version__`
|
|
to 0.35.0 with `ip_in_cidrs` and `request_ip_allowlisted` named as the new
|
|
surface (`shopdb/__init__.py:143-152`), and made
|
|
`plugins/usb/api/routes.py:42` and `plugins/printedparts/api/routes.py:521`
|
|
import `request_ip_allowlisted`. Neither manifest was touched: usb still says
|
|
`">=0.1.0,<1.0.0"` and printedparts `">=0.16.0,<1.0.0"`. The team does maintain
|
|
this field - `066a19052` raised `plugins/endpoints/manifest.json:7` to
|
|
`">=0.34.0,<1.0.0"` one commit earlier.
|
|
|
|
On a 0.34.0-or-older core both plugins would pass the loader's version gate
|
|
(`shopdb/plugins/loader.py:141-166`, called at `:258`) and then die with an
|
|
ImportError at blueprint registration. Impact at the reference site is nil, because core and
|
|
plugins always ship from this one tree.
|
|
|
|
The ADR-002 framing is weak and should not be quoted as a clean breach: ADR-002
|
|
itself says pre-1.0 plugins should declare `">=0.1.0,<1.0.0"`, which is
|
|
literally what `plugins/usb/manifest.json:8` declares, while its Neutral section
|
|
says bundled plugins' `core_version` is "always the current
|
|
`__contract_version__`". The ADR contradicts itself here. This is staleness
|
|
against team practice.
|
|
|
|
## Checked and not a violation
|
|
|
|
These were examined and did NOT hold up as ADR breaches. They are listed so the
|
|
next review does not re-open them.
|
|
|
|
- **`get_settings_routes` narrowed to a one-hub allowlist without a contract
|
|
bump.** `shopdb/core/api/plugins.py:92` now filters through
|
|
`renderable_settings_routes`. Nothing observable changed: the only frontend
|
|
caller, `frontend/src/views/settings/ModelCatalogHub.vue:73`, already filtered
|
|
to its own hub at `:76`, and `frontend/src/router/routes/core.js:62-63` states
|
|
in-tree that no other hub page fetches or registers plugin routes. A route
|
|
naming another hub was drawn nowhere before and is drawn nowhere now. No
|
|
`BasePlugin` ABC signature or `PluginMeta` schema changed, so under ADR-002
|
|
this is patch-level and no bump was owed. It is also not silent:
|
|
`shopdb/plugins/base.py:46-52` logs an error naming the plugin, the hub and
|
|
the supported set, and the same commit added the "Constraints as implemented"
|
|
section to the ADR-019 draft plus
|
|
`tests/test_core/test_plugin_settings_hubs.py`.
|
|
- **`computers` and `printers` manifests declaring `>=0.20.0` against the
|
|
contract-0.33.0 key `acceptsagentcredential`.** ADR-002 requires no such
|
|
tracking, nothing in `loader.py` implements it, and the claimed fail-open
|
|
cannot occur: on a core pinned at 0.32.x the endpoints plugin is refused at
|
|
load by its own `>=0.34.0` floor, so no credential is ever issued and there is
|
|
no enrolled PC whose shared key an older dispatcher could wrongly accept.
|
|
|
|
## Drift that no ADR covers
|
|
|
|
Recorded because it is real and someone will look for it here.
|
|
|
|
**`docs/openapi.json` is a release behind the code.**
|
|
`shopdb/core/api/docs.py:18` pins `_SPEC` to that file and serves it through a
|
|
vendored Redoc at `/api/docs`, specifically for the air-gapped production box.
|
|
The tracked spec holds 377 paths and `info.version` is still `0.12.0`. It
|
|
contains zero paths matching `enroll` or `bootstrap`, while
|
|
`git diff 2ac2e6d33..gh-wj -- 'plugins/*/api/*.py' 'shopdb/core/api/*.py'` adds
|
|
24 route decorators, including the whole enrollment surface
|
|
(`plugins/endpoints/api/enrollment.py:289`, `:580`, `:657`, `:752`, `:773`,
|
|
`:827`, `:853`, `:1095`, `:1135`, `:1233`, `:1253`) and the bootstrap-token
|
|
surface (`plugins/endpoints/api/bootstrap.py:419`, `:474`, `:485`, `:506`,
|
|
`:530`). Where the spec does describe a route that changed, it describes the old
|
|
behaviour: `/api/plugins/settings-routes` is documented as
|
|
"**Auth:** none (jwt-optional decorator, deliberately anonymous...)" with
|
|
`"security": [{}, {"bearerAuth": []}]`, while the shipped handler is
|
|
`@require_permission('settings.view')`.
|
|
|
|
Neither ADR-001 nor ADR-002 mentions `openapi.json` or `api-inventory.json`, so
|
|
no stated rule is broken. The drift also cannot be detected or repaired from
|
|
this branch: `docs/api-inventory.json` is not tracked here, so
|
|
`tests/test_api_inventory_parity.py` skips and `scripts/gen_openapi.py` has no
|
|
input.
|
|
|
|
**`CLAUDE.md` in this repo contradicts the repo.** Line 3 and line 9 describe
|
|
the database as "PostgreSQL 15+", while `docker-compose.yml:20` runs
|
|
`mysql:8.4` and `.env.example:29` is a `mysql+pymysql://` URL. Line 31 states
|
|
"**Columns**: snake_case (e.g., `serial_number`, `last_seen`)", which is the
|
|
opposite of `CONTRIBUTING.md:17` ("Column names: lowercase, concatenated,
|
|
singular. No dashes, no underscores") and of what the gate enforces at
|
|
`scripts/check-naming-and-style.sh:236`. Line 119 carries the ADR-016 device
|
|
token misattribution described above. `CLAUDE.md` is outside this folder and was
|
|
not edited.
|
|
|
|
## Scope note
|
|
|
|
This page records ADR conformance only. The same review produced security
|
|
findings against the per-PC credential surface, the kiosk network admission and
|
|
the one-line bootstrap; those are not ADR matters and are recorded in
|
|
`Security-Audit-2026-09-11.md`, refreshed on the same date and the same commit.
|
|
Violation 3 on this page and finding 31 on that one are the same ADR-016
|
|
misattribution seen from two angles. The highest of the security findings, for
|
|
cross-reference, is that `install.ps1` writes the fleet shared key to
|
|
`HKLM\SOFTWARE\GE\ShopDB` with no ACL
|
|
(`plugins/endpoints/api/siteagent.py:328`, helper generated at `:394-411`, no
|
|
`SetAccessControl` or `RegistrySecurity` anywhere in the generated installer),
|
|
where any logged-on user can read it - a consequence the repo states itself at
|
|
`.github/GE-ENFORCE-COLLECTOR-FIX.html:377-380`.
|
|
|
|
## References
|
|
|
|
- ADR set: `docs/adr/ in the product repo` (NOT on this branch)
|
|
- `scripts/check-naming-and-style.sh` - the only ADR-aware gate in this repo
|
|
- `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md` - the rollout runbook
|
|
- ADR-019-Plugin-Dynamic-Settings-Routes.md - the one ADR-shaped file here
|
|
- Security-Audit-2026-09-11.md - the security half of the same review
|
|
- Architectural-Findings-2026-09-11.md - re-checked 2026-09-18
|
|
- Maintenance-Footprint-Baseline.md - re-checked 2026-09-18
|