Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ from darnit_baseline.controls import level1
# CORRECT — use plugin discovery
from darnit.core.discovery import get_implementation
impl = get_implementation("openssf-baseline")
controls = impl.get_all_controls()
config_path = impl.get_framework_config_path()
```

---
Expand Down Expand Up @@ -380,13 +380,8 @@ class ComplianceImplementation(Protocol):
@property
def spec_version(self) -> str: ... # "OSPS v2025.10.10"

def get_all_controls(self) -> list[ControlSpec]: ...
def get_controls_by_level(self, level: int) -> list[ControlSpec]: ...
def get_rules_catalog(self) -> dict[str, Any]: ...
def get_remediation_registry(self) -> dict[str, Any]: ...
def get_framework_config_path(self) -> Path | None: ...
def register_controls(self) -> None: ...
# Optional: register_handlers() — checked via hasattr()
# Optional: register_handlers() — the handler hook, checked via hasattr()
```

### Entry Point Registration
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(`classify_writeback`), the threat-model classes in
`darnit_baseline.threat_model.models` other than `StrideCategory`, and
`darnit.remediation.RemediationResult.to_markdown` (#487).
- **BREAKING:** `ComplianceImplementation` no longer declares
`get_all_controls`, `get_controls_by_level`, `get_rules_catalog`,
`get_remediation_registry`, or `register_controls`, and the framework no
longer calls `register_controls()`. No production path called the others.
The protocol is `name`, `display_name`, `version`, `spec_version`, and
`get_framework_config_path()`; controls, SARIF rules, and remediations come
from the framework TOML (`load_framework_by_name`,
`load_controls_from_framework`). A plugin that still defines the methods is
discovered as before. The in-tree implementations drop them, and
`darnit-example` drops its `_RULES` catalog, `remediation/registry.py`, and
empty `controls` package (#487).

### Added

Expand Down Expand Up @@ -323,6 +334,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **BREAKING:** `register_handlers()` is the one handler registration hook
for plugins (#451). `darnit-gittuf` and `darnit-reproducibility` rename
`register_sieve_handlers()` to `register_handlers()`, and their `register()`
entry points no longer register handlers during discovery;
`darnit-example` defines only `register_handlers()`; `darnit-csl` no longer
registers `csl_llm_if_present` when `darnit_csl` is imported (call
`CommunitySpecImplementation().register_handlers()`, which the framework
does on every audit and when a framework load needs the step type); `darnit-hello` defines it as a
no-op. The framework still calls `register_sieve_handlers()` on
out-of-tree plugins, for compatibility only.
- **BREAKING:** framework loading is strict. An unknown control key, a step
key that is neither a common step field nor a setting its step type
declares (a misspelled `fail_on_mis`, for example), an `expr` its step
Expand Down
30 changes: 13 additions & 17 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ from darnit_baseline.controls import level1
from darnit.core.discovery import get_implementation
impl = get_implementation("openssf-baseline")
if impl:
controls = impl.get_all_controls()
config_path = impl.get_framework_config_path()
```

### Rule 2: Implementations MAY Import Framework
Expand All @@ -78,14 +78,12 @@ impl.name # str: Implementation identifier
impl.display_name # str: Human-readable name
impl.version # str: Implementation version
impl.spec_version # str: Spec version implemented
impl.get_all_controls() # List[ControlSpec]: All controls
impl.get_controls_by_level(n) # List[ControlSpec]: Controls at level n
impl.get_rules_catalog() # Dict: SARIF rule definitions
impl.get_remediation_registry() # Dict: Auto-fix mappings
impl.get_framework_config_path() # Path | None: TOML config location
impl.register_controls() # None: Register TOML controls
impl.register_handlers() # None: optional hook, registers Python handlers
```

Those five members are the whole protocol; `register_handlers()` is optional. Controls, SARIF rules, and remediations come from the framework TOML, never from the implementation. `get_all_controls`, `get_controls_by_level`, `get_rules_catalog`, `get_remediation_registry`, and `register_controls` were removed (#487); a plugin that still defines them is discovered, and they are not called.

## Plugin System

### Entry Points
Expand All @@ -103,8 +101,8 @@ openssf-baseline = "darnit_baseline:register"

```python
# my_framework/implementation.py
from importlib.resources import files
from pathlib import Path
from darnit.core.plugin import ComplianceImplementation, ControlSpec

class MyFrameworkImplementation:
@property
Expand All @@ -123,15 +121,11 @@ class MyFrameworkImplementation:
def spec_version(self) -> str:
return "MySpec v1.0"

def get_all_controls(self) -> list[ControlSpec]:
# Return your control definitions
...

def get_framework_config_path(self) -> Path | None:
return Path(__file__).parent / "my-framework.toml"
return Path(str(files(__package__) / "my-framework.toml"))

def register_controls(self) -> None:
pass # Controls are defined in TOML; no Python registration needed
def register_handlers(self) -> None:
pass # Optional: register custom step types (see Handler Registration)
```

2. Add the registration function:
Expand Down Expand Up @@ -318,7 +312,9 @@ Remediation plans, then applies only what it planned (feature 043; framework-des

### Handler Registration

Plugins register handlers using the `register_handlers()` method:
`register_handlers()` is the protocol hook for a plugin's Python handlers: sieve step types (`get_sieve_handler_registry().register(...)`, with ceiling and settings, framework-design.md 3.0.3) and MCP tool handlers. The framework calls it on every audit, on `darnit list`, and when loading a framework names a step type that is not yet registered, so it must be safe to call repeatedly. `register_sieve_handlers()` is still called for compatibility with out-of-tree plugins, but is not a supported choice; no in-tree plugin uses it. Registering at module import works but is not the protocol: the framework cannot introspect it, and a registry reset loses the handlers (#451; framework-design.md 6.4).

MCP tool handlers, for example:

```python
class MyImplementation:
Expand Down Expand Up @@ -373,10 +369,10 @@ Always handle missing implementations gracefully:
```python
impl = get_implementation("openssf-baseline")
if impl:
result = impl.get_all_controls()
config_path = impl.get_framework_config_path()
else:
logger.warning("No implementation found")
result = []
config_path = None
```

## Technology Stack
Expand Down
18 changes: 11 additions & 7 deletions docs/HANDLER_AUTHORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,11 @@ Guidelines for custom handlers:
## 5. Registering a Custom Handler

Custom sieve handlers are registered with the `SieveHandlerRegistry` in your
implementation's `register_sieve_handlers()` method:
implementation's `register_handlers()` method, the protocol hook the framework
calls before it loads or audits your framework (framework-design.md 6.4):

```python
def register_sieve_handlers(self) -> None:
def register_handlers(self) -> None:
from darnit.sieve.handler_registry import get_sieve_handler_registry
from . import handlers

Expand Down Expand Up @@ -325,10 +326,13 @@ steps = [
]
```

Important distinction:

- `register_sieve_handlers()` is for `[[controls."...".passes]]` handlers.
- `register_handlers()` is for MCP tool handlers under `[mcp.tools.*]`.
`register_handlers()` registers both kinds of handler: sieve step types for
`[[controls."...".passes]]` (in `get_sieve_handler_registry()`) and MCP tool
handlers for `[mcp.tools.*]` (in `darnit.core.handlers.get_handler_registry()`).
A method named `register_sieve_handlers()` is still called for compatibility
with older plugins, but new plugins should not use it. Registering at module
import works, but the framework cannot see it, and a registry reset loses the
handlers.

## 6. Testing One Control at a Time

Expand Down Expand Up @@ -396,7 +400,7 @@ steps = ["Confirm the README body explains the project"]

2. Add the handler function to `src/<your_package>/handlers.py`.

3. Register it in `register_sieve_handlers()`.
3. Register it in `register_handlers()`.

4. Test the handler directly with pytest.

Expand Down
Loading
Loading