Apply skill-driven review fixes: security, hook isolation, tests, docs

Addresses findings from a 6-lens review against the project skills
(defining-asset-contract, enforcing-plugin-contract, hardening-flask-config,
integrating-plugin-hooks, pinning-flask-behavior, simplifying-python).

Security (hardening-flask-config):
- Load per-plugin COLLECTOR_API_KEY_<PLUGIN> from env in create_app. from_object
  only copies class attributes, so per-plugin keys (ADR-006) were dead in real
  deploys and silently fell back to the shared key.
- EMPLOYEE_DB_USER/PASSWORD no longer default to root/rootpassword (no safe
  default for a secret; unset fails loud). Documented in .env.example + DEPLOY.md.
- COLLECTOR_API_KEY + per-plugin + EMPLOYEE_DB_* added to .env.example/DEPLOY.md.

Hook isolation (integrating-plugin-hooks):
- collector _collector_plugins and dashboard get_navigation now re-raise in
  dev/test and log+isolate in prod, instead of silently swallowing a broken
  plugin hook.

Plugin loader (enforcing-plugin-contract):
- enable_plugin/install_plugin read dependencies+version from the manifest
  instead of instantiating the plugin class.
- _register_plugin_components rejects a second plugin claiming an already-used
  api_prefix (reset per app in init_app).

Tests (pinning-flask-behavior):
- test_identifiers.py: gauge/maintenance round-trip on computer/printer/network
  create+update; per-type seed yields the 12 identifier keys.
- contract tests for apply_collector_payload presence + schema-declarers-implement.
- security tests for per-plugin key env loading + no employee-db password default.

Docs/contract sync (defining-asset-contract):
- PLUGIN-HOOKS.md documents apply_collector_payload; stale 0.2.0 -> 0.3.0.
- ADR-006 documents apply_collector_payload + single-dispatch rationale.
- ADR-001 enumerates the expanded shopdb.api import surface.

Simplify (simplifying-python):
- De-duplicate the 21-entry settings defaults: shared build_default_settings()
  used by both the /settings/seed route and the CLI (were drifting copies).
- Remove dead AssetStatus import + redundant AssetType local import in computers
  plugin; comment the statusid=1 collector default.

153 tests pass (was 145), naming/style green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 19:25:52 -04:00
parent f663cc5bbe
commit 5fa5160420
16 changed files with 273 additions and 198 deletions

View File

@@ -9,7 +9,7 @@ The contract is locked in [ADR-001](../docs/adr/ADR-001-asset-as-platform-contra
The framework declares its contract version in `shopdb/__init__.py`:
```python
__contract_version__ = '0.2.0'
__contract_version__ = '0.3.0'
```
Each plugin's `manifest.json` declares the range of contract versions it supports:
@@ -222,6 +222,31 @@ class ComputersPlugin(BasePlugin):
If the hook returns `None` (the default), no collector endpoint is registered.
### `apply_collector_payload(payload: Dict) -> Dict`
Companion to `get_collector_schema` (ADR-006). The generic
`/api/collector/<pluginname>` endpoint calls this after the payload passes
identity validation, to idempotently upsert an asset. Return a dict with at
least `action` (`created` | `updated` | `noop`), `assetid`, and `warnings`
(list).
This is a CONDITIONAL hook: it is only required when `get_collector_schema`
returns non-None. The BasePlugin default raises `NotImplementedError` (the
dispatcher turns that into a 500), so a plugin that declares a schema but
forgets the upsert fails loud. Plugins with no collector schema never need it.
The `test_schema_declaring_plugins_implement_apply` contract test enforces the
pairing.
```python
def apply_collector_payload(self, payload):
host = payload['hostname']
comp = Computer.query.filter(Computer.hostname.ilike(host)).first()
action = 'updated' if comp else 'created'
# ... create-or-update Asset + extension ...
db.session.commit()
return {'action': action, 'assetid': comp.assetid, 'warnings': []}
```
## Lifecycle hooks
These run when the plugin's installation state changes. All optional.