Skip to content

fix(core): resolve every configured module path under one policy (#490) - #563

Merged
mlieberman85 merged 2 commits into
darnitdevorg:mainfrom
mlieberman85:fix-490-handler-import-policy
Oct 7, 2026
Merged

mlieberman85 merged 2 commits into
darnitdevorg:mainfrom
mlieberman85:fix-490-handler-import-policy

Conversation

@mlieberman85

Copy link
Copy Markdown
Contributor

Summary

module:function strings from configuration were turned into imports in two places, with two different policies:

  • HandlerRegistry checked a hard-coded, out-of-date prefix list. It rejected darnit_csl, darnit_gittuf, darnit_reproducibility and darnit_hello.
  • ToolRegistry.load_handler, which loads [mcp.tools.*] at server start, imported any module. Before this change, a tool spec with handler = "os:system" registered as an MCP tool.

With this change:

  • One resolver. darnit.core.handlers.resolve_module_path contains the only importlib.import_module call in packages/darnit/src. A test enforces that.
  • One derived policy. A module's top-level package must be darnit, or the module of an implementation that discovery accepted from the darnit.implementations entry points (e.g. darnit_csl:register gives darnit_csl). A plugin skipped by discovery or verification is excluded. The path must have exactly one :, and every part must be an identifier.
  • Checked before import. A refused module is never imported.
  • Refusals fail loudly. HandlerImportRefused (a ValueError) names the path and the allowed packages, and is logged at WARNING. The server factories skip a refused tool and log an ERROR naming it.
  • Old tuples removed. All three ALLOWED_MODULE_PREFIXES tuples are gone, including the copies in the dead adapter code.

Docs:

  • docs/architecture/framework-design.md section 6.5 was updated first, with two new scenarios.
  • THREAT_MODEL.md: the dynamic-import findings are collapsed into one entry that describes the control as it now holds.
  • docs/IMPLEMENTATION_GUIDE.md and docs/SECURITY_GUIDE.md no longer tell plugin authors to extend the prefix list.

Fixes #490

Type of Change

  • Bug fix (non-breaking change fixing an issue)
  • New feature (non-breaking change adding functionality)
  • Breaking change (fix or feature causing existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)

HandlerRegistry.get_handler now raises for a refused path instead of returning None; the CHANGELOG lists this under Removed and Security.

Framework Changes Checklist

  • Updated framework spec (docs/architecture/framework-design.md) if behavior changed
  • Ran uv run python scripts/validate_sync.py --verbose and it passes

Testing

  • Tests pass locally (uv run pytest tests/ -v): 5162 passed, 29 skipped
  • Added tests for new functionality (if applicable): tests/darnit/core/test_module_path_policy.py (31 cases), plus factory and registry tests
  • Linting passes (uv run ruff check .)

New coverage:

  • Refusals: os:system, subprocess:run, builtins:eval, unknown packages, and malformed paths.
  • Acceptances: every shipped plugin's modules.
  • TestShippedFrameworkTools starts every installed framework's server and checks that each declared tool registers (baseline 18 of 18, CSL 3 of 3, and so on).
  • The integration tests pass with GitHub Actions environment variables set (334 passed).

AI assistance

  • No AI assistance was used
  • AI assistance was used

Claude (Claude Code, claude-opus-5-5) made this change: spec, code, and tests. This description was also drafted with Claude. Commits carry an Assisted-by: Claude:claude-opus-5-5 trailer.

Additional Notes

packages/darnit-testchecks/testchecks.toml declares a Python adapter module (darnit_testchecks.adapters.builtin) that this policy would refuse, because testchecks registers under darnit.frameworks, not darnit.implementations. Nothing loads it through these paths today; the adapter loaders have no production caller. Deleting those loaders under #487 is better than widening the policy.

🤖 Generated with Claude Code

Section 6.5 described a prefix whitelist "discovered from entry points"
that the code never implemented, and did not cover the MCP tool loader,
which imported any module named in [mcp.tools]. State the contract the
fix implements: one resolver for every module:attribute import, a top-level
package of darnit or an installed implementation's entry point module,
refusal before import, and refused tools reported at server start.

Refs darnitdevorg#490

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Michael Lieberman <mlieberman85@gmail.com>
ToolRegistry.load_handler, the path that loads [mcp.tools] handlers at
server start, imported any module:function string with no check, while
HandlerRegistry and the two adapter registries each kept a hardcoded
prefix tuple that missed darnit_csl, darnit_gittuf, darnit_reproducibility
and darnit_hello. The shipped community-spec tool loaded only through the
unchecked path.

All four now call darnit.core.handlers.resolve_module_path. It refuses a
malformed path or a top-level package other than darnit or the module of
a discovered darnit.implementations entry point, before importing. The
allowed set is recorded by discovery and reset with its cache. A refusal
raises HandlerImportRefused (a ValueError) naming the path and allowed
packages; HandlerRegistry.get_handler raises it rather than returning
None, and the server factory logs the refused tool at ERROR and does not
register it.

Fixes darnitdevorg#490

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Michael Lieberman <mlieberman85@gmail.com>
@mlieberman85
mlieberman85 merged commit c082f60 into darnitdevorg:main Oct 7, 2026
8 checks passed
@mlieberman85
mlieberman85 deleted the fix-490-handler-import-policy branch October 7, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handler module allowlist is bypassed by MCP tool loading and does not match the installed plugin set

1 participant