Skip to content

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:

  1. built-ins (fixed order),
  2. bundled contrib adapters,
  3. in-process registrations (registration order),
  4. 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 with AdapterReadError before it is read into memory (size is checked via stat).
  • 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_tokens refuses 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.