09 — Pluggable Import Adapters & Audit Governance (P2-26)¶
This document describes the P2-26 changes: a real extension point for import adapters, hardening of the untrusted-input path, and governance audit events for the lifecycle actions that change a repo's standards posture.
1. Pluggable adapter registry (B1)¶
The brownfield importer (cortexbuild adopt, P2-03/04) previously hard-coded its
adapter set as a tuple, so adding a new foreign source meant editing core. The
registry now resolves adapters through three channels — a new source can be
contributed with zero edits to core:
| Channel | API | Use |
|---|---|---|
| Built-ins | adopt.adapters.DEFAULT_ADAPTERS |
The curated first-party set. |
| Bundled contrib | adopt.contrib.CONTRIB_ADAPTERS |
First-party adapters shipped through the pluggable path (e.g. JetBrains). |
| In-process registration | adopt.register_adapter(cls_or_instance) |
A class (decorator-friendly) or instance registered at import time. |
| Entry points | group cortexbuild.import_adapters |
A third-party distribution advertises an adapter discovered at runtime. |
adopt.load_adapters() is the single dispatch source used by
discover_and_normalise. It is deterministic:
- built-ins (fixed order),
- bundled contrib adapters,
- in-process registrations (registration order),
- entry-point adapters (sorted by
(entry-point name, adapter id)).
Duplicates are de-duplicated by adapter id, first occurrence wins, so a
built-in can never be silently shadowed by a later plugin claiming the same id.
A broken third-party entry point is skipped, never fatal.
JetBrains adapter¶
adopt.contrib.jetbrains.JetBrainsAdapter is a net-new adapter shipped through
the contrib channel (not the built-in tuple) to prove the mechanism end-to-end.
It recognises Junie .junie/guidelines.md and AI Assistant .aiassistant/rules/*.md.
2. Untrusted-input hardening (S6)¶
Foreign artefacts are untrusted. Limits and the brand-token scanner live in
adopt/base.py and are enforced centrally so every adapter — built-in or
plugin — inherits them and cannot opt out:
MAX_ARTEFACT_BYTES(1 MiB): a larger file is refused withAdapterReadErrorbefore it is read into memory (size is checked viastat).MAX_FRONTMATTER_BYTES(64 KiB) + key validation: an oversized frontmatter block, or one whose keys are not all strings, is treated as no frontmatter rather than carried downstream.- Path-traversal confinement: a discovered source (or symlink) that resolves
outside the repo being adopted is dropped (
registry._is_within_repo). - Brand-token guard:
assert_no_forbidden_tokensrefuses a normalised body that smuggles a CortexBuild brand token. It is enforced at the persistence boundary (stage_artefacts) so every caller is guarded, while the read-only reconcile path keeps redacting tokens in its report rather than raising.
3. Governance audit events (B2)¶
Lifecycle actions that change standards posture now append a record to the
append-only audit ledger (P2-24) via the best-effort helper
cortexbuild_core.audit.emit_event:
| Action | Event kind | Emitted from |
|---|---|---|
cortexbuild adopt |
adopt |
adopt/cli.py after staging |
| missing-context override | override |
context/gate.append_override |
cortexbuild standards sync |
sync |
standards/cli.py |
cortexbuild skills propose |
promotion |
learning/cli.py |
emit_event is best-effort (it never breaks the action it records) and the
underlying append is idempotent + concurrency-safe (PR1), so re-running an
action de-duplicates rather than double-logging. Events are emitted at the CLI /
chokepoint layer — not inside pure library functions — so existing byte-stability
and directory-content tests are unaffected.