The first post in this series introduced git-fabric and why it exists. This post opens the hood on @git-fabric/cve — the first real app built on that philosophy — and walks through the five-layer architecture, the adapter interfaces that make it portable, and the gateway that ties everything together behind a single MCP connection.

Why Layers

Most security tools are monoliths. They scan, enrich, decide, and act in one pass. That’s fine until you want to do any one of those things independently — enrich a CVE you already know about, re-triage with a different severity policy, or inspect the queue without triggering a scan.

The @git-fabric/cve package splits the work into five discrete layers, each independently callable:

Detection  ->  Intelligence  ->  Decision  ->  Action  ->  State
   |               |               |            |           |
   Scan deps       NVD enrich      Policy       Branch      JSONL queue
   query GHSA      CVSS/CWE        triage       commit      dedup
                                   plans        PR          stats

The first three layers — Detection, Intelligence, Decision — are pure. They read data, transform it, and return results. No side effects. No branches created, no files written, no state mutated. You can call them in a test, in a dry run, or in a pipeline, and nothing changes in the outside world.

The last two layers — Action and State — are effectful. Action creates branches, commits dependency bumps, and opens PRs. State manages the persistent CVE queue. Side effects are isolated to these two layers, which means the blast radius of any bug is contained and auditable.

Walking the Layers

Detection reads dependency manifests and queries the GitHub Advisory Database (GHSA). Given a repository, it returns a list of raw advisory matches — CVE IDs, affected packages, version ranges. It doesn’t judge severity or decide what to do. It just finds what’s there.

Intelligence takes those raw advisories and enriches them from NVD. CVSS scores, severity ratings, CWE classifications, current status. The output is a fully characterized vulnerability — everything you need to make a decision, but no decision made yet.

Decision applies a severity policy to produce triage plans. The default policy is straightforward:

SeverityDefault ActionPR Type
CRITICALAuto-PRConfirmed
HIGHAuto-PRDraft
MEDIUMSkipManual review
LOWSkipNoise reduction

Critical and high vulnerabilities get automatic PRs. Medium and low get logged but not acted on — a deliberate yarn over in the automation fabric, where human judgment matters more than speed. The policy is a plain object, not buried in conditionals. Swap it and the entire triage behavior changes.

Action takes a triage plan and executes it. Create a branch, commit the dependency bump, open the PR with the right labels and references. This is the only layer that talks to the GitHub API with write permissions.

State manages the CVE queue as a JSONL file. Deduplication, filtering, compaction, and statistics. Every scan appends; nothing overwrites. The queue is the audit trail.

The Project Structure

The code mirrors the architecture directly:

src/
  types.ts              # Shared types + adapter interfaces
  layers/
    detection.ts        # GHSA scanning + manifest parsing
    intelligence.ts     # NVD enrichment
    decision.ts         # Severity policy engine
    action.ts           # Branch + commit + PR creation
    state.ts            # JSONL queue management
  mcp/
    server.ts           # MCP server (7 tools)
  adapters/
    env.ts              # Env var -> Octokit adapter

No barrel files hiding indirection. Each layer is one file. The directory structure is the architecture diagram.

Adapter Interfaces

Here’s the design choice that makes the whole thing portable. The CVE app doesn’t import Octokit directly. It doesn’t know how to authenticate with GitHub. It doesn’t know where the state file lives. Instead, it defines what it needs through adapter interfaces:

interface GitHubAdapter {
  getAdvisories(owner: string, repo: string): Promise<Advisory[]>;
  createBranch(owner: string, repo: string, ref: string): Promise<void>;
  createPR(owner: string, repo: string, params: PRParams): Promise<PR>;
  // ...
}

interface StateAdapter {
  append(entry: QueueEntry): Promise<void>;
  list(filter?: QueueFilter): Promise<QueueEntry[]>;
  compact(retention: number): Promise<CompactResult>;
  stats(): Promise<QueueStats>;
}

The app declares the shapes. The consumer provides the implementations. When git-steer consumes @git-fabric/cve, it passes an Octokit-backed GitHubAdapter with rate-limit throttling and retry logic baked in (critical, given a prior GitHub account suspension from aggressive automated API patterns). When tests consume it, they pass mocks. When a future GitHub Action consumes it, it can pass its own adapter with the GITHUB_TOKEN from the runner environment.

This is dependency inversion at the package boundary. The CVE app is a library with holes shaped like its dependencies. Plug in the right adapters and it runs anywhere — inside git-steer, inside Claude Desktop via MCP, inside CI, or standalone.

The Gateway

Individual fabric apps are useful on their own. But the real power shows up when you aggregate them behind a gateway.

The gateway has three parts:

Registry — an in-memory map of app names to loaded FabricApp instances. Each app registers itself with createApp(), which returns a standard contract:

interface FabricApp {
  name: string;
  version: string;
  tools: ToolDefinition[];
  health(): Promise<HealthStatus>;
}

Router — takes an incoming tool call, looks up which app owns that tool name, and dispatches the call. Health checks are aggregated across all registered apps. One route handler replaces N individual case blocks.

Loader — handles dynamic app loading with graceful degradation. If @git-fabric/cve fails to load (missing token, network error, bad config), the gateway continues without it. The core tools still work. The fabric tools simply don’t appear in the tool list.

In git-steer, the entire gateway integration is 65 lines. Initialize the gateway, register the CVE app, get back a handle. If registration fails, log it and move on. The tool list is built conditionally — 32 core tools always present, 6 fabric tools added only if the gateway loaded successfully.

MCP as the Surface

Every layer in the CVE app is exposed through MCP tools:

MCP ToolLayerSide Effects
cve_scanDetectionNo
cve_enrichIntelligenceNo
cve_triageDecisionNo
cve_batchDetection + Intelligence + DecisionNo
cve_queue_listStateNo
cve_queue_statsStateNo
cve_queue_updateStateYes

The cve_batch tool is a convenience that chains the three pure layers in sequence — scan, enrich, triage — but each layer remains independently callable. You can enrich a CVE you found through other means. You can re-triage with different policy parameters. The tools are composable because the layers are composable.

Through the gateway, these tools are available under a fabric_cve_* namespace. The gateway adds its own tools — fabric_health, fabric_apps, fabric_route — and aggregates everything into a single MCP connection. One server, N apps, all discoverable through ListTools.

Why This Matters

The bet behind this architecture is that N=2 is coming soon. The CVE app is the first fabric app. Compliance scanning, drift detection, dependency license auditing — they all follow the same pattern: layers that read, layers that decide, layers that act, adapters that abstract the environment, and a gateway that aggregates the surface.

When the second app lands, the gateway is already there. Register it, add tool definitions, extend the routing map. The infrastructure cost of going from one app to two is close to zero because the patterns were designed for composition from the start.

That’s the whole point of thinking in fabric rather than in monoliths. Every thread is independent. Every connection is loose enough to undo. And the loom holds it all under just enough tension to weave.


This is part 2 of the git-fabric series. Part 1: Introducing git-fabric. Source: github.com/git-fabric.