* feat(security): add plugin content integrity verification (SHA256) SDK-side follow-up to molecule-core PR #1019 (pinned-ref supply-chain fix). Changes: - verify_plugin_sha256(plugin_dir, expected_sha) — content-addressed manifest hash over sorted (relpath, SHA256(content)) pairs; plugin.yaml excluded from its own hash to avoid circular dependency - _walk_files(root) / _sha256_file(path) — internal helpers - install_plugin() calls verify_sha256 after atomic rename; on mismatch deletes plugin dir and raises ValueError before setup.sh runs - PLUGIN_YAML_SCHEMA gains optional sha256 field (64-char lowercase hex) - validate_manifest() validates sha256 format when present Tests (12 new): - sha256_file correctness, walk_files ordering, verify_* (match/mismatch/invalid) - install_plugin sha256 verified: setup.sh runs - install_plugin sha256 mismatch: raises ValueError, setup.sh NOT run - install_plugin no sha256: backward-compat, skips verification - validate_manifest sha256: valid/invalid/non-hex/absent Pre-existing: 4 async tests in test_sdk.py fail without pytest-asyncio (not related to this change). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): add pytest-asyncio markers to async adaptor tests The 4 tests using async def were failing because pytest-asyncio was not installed and pytest.ini set asyncio_mode=auto (which requires it). Add @pytest.mark.asyncio to each async test and add pytest-asyncio as a test optional dependency so CI gets the right extras when installing. Fixes: 4 FAILED tests in test_sdk.py * feat(cli): add verify-sha256 command to molecule_agent Add `python -m molecule_agent verify-sha256 <plugin-dir>` CLI that computes the content-integrity SHA256 for a plugin directory (the same manifest hash that verify_plugin_sha256() uses internally). Plugin authors can run this to generate the hash to put in plugin.yaml's sha256 field. Also: - Re-export verify_plugin_sha256 and compute_plugin_sha256 from the molecule_agent package root so `from molecule_agent import compute_plugin_sha256` works. - Update CLAUDE.md to document the CLI and content integrity flow. - Write pr-description-draft.md as a backup for when GH_TOKEN recovers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Molecule AI SDK-Dev <sdk-dev@agents.moleculesai.app> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
3.8 KiB
PR Description Draft — Plugin Content Integrity (SHA256)
File: pr-description-draft.md in the SDK repo, to be pasted into GitHub when token recovers.
feat(security): add plugin content integrity verification (SHA256)
Problem
When a workspace installs a plugin via GET /workspaces/:id/plugins/:name/download, the platform can pin the tarball to a specific Git ref (PR #1019, molecule-core). However, the SDK had no content-integrity check: once a tarball was served under a valid pinned ref, the SDK would extract it and run setup.sh without verifying the unpacked content matched the declared SHA256 in plugin.yaml.
A supply-chain attacker who compromised the plugin registry or the GitHub source could serve a tampered tarball under a valid pinned ref. The install would proceed, setup.sh would run with plugin author credentials, and the attacker's payload would execute.
Solution
Add a content-addressed manifest hash to plugin.yaml and verify it before running setup.sh.
Manifest format: SHA256 of the canonical JSON of sorted((relative_path, SHA256(file_content)) for all files except plugin.yaml itself). plugin.yaml is excluded from its own hash because it contains the hash — otherwise the bootstrap is circular.
Why this works: Even if an attacker replaces a file, they cannot compute the matching manifest hash without knowing the excluded set. The platform pins the tarball by Git ref; the SDK verifies the tarball's unpacked content integrity before execution.
Changes
| File | Change |
|---|---|
molecule_agent/client.py |
Added verify_plugin_sha256(), _walk_files(), _sha256_file(), integrated into install_plugin() before setup.sh runs |
molecule_agent/__main__.py |
Added CLI: python -m molecule_agent verify-sha256 <plugin-dir> to compute the hash for a plugin directory |
molecule_plugin/manifest.py |
Added sha256 field to PLUGIN_YAML_SCHEMA, validation in validate_manifest() |
molecule_agent/__init__.py |
Re-export verify_plugin_sha256 and compute_plugin_sha256 |
tests/test_remote_agent.py |
12 new tests covering all sha256 paths, including integration with install_plugin() |
known-issues.md |
Updated KI-006 with resolution |
CLAUDE.md |
Added content integrity section documenting the verify-sha256 CLI |
API / Schema
plugin.yaml additions:
name: my-plugin
version: "1.0"
sha256: a3f5b8c9d1e2... # 64 lowercase hex chars; generate with: python -m molecule_agent verify-sha256 <plugin-dir>
Generate the hash for a local plugin directory:
python -m molecule_agent verify-sha256 ./my-plugin
# Outputs: "Computed SHA256: <64-char hash>"
# Copy the hash into plugin.yaml under the sha256 field.
Security notes
- The hash excludes
plugin.yamlitself to avoid circular dependency. This meansplugin.yamlcan be modified freely as long as the new hash is recomputed and stored. setup.shis only executed afterverify_plugin_sha256()succeeds. If verification fails, the staging directory is cleaned up andsetup.shis never called._safe_extract_tar()(tar-slip protection) andverify_plugin_sha256()(content integrity) address two separate concerns and are applied in sequence.
Test results
tests/test_remote_agent.py: 57 passed (12 new sha256 tests)
tests/test_sdk.py: 50 passed
tests/test_validators.py: 36 passed
Total: 143 passed
Migration path for existing plugins
Plugin authors who want to pin their plugin must:
- Run
python -m molecule_agent verify-sha256 <plugin-dir>on the final directory - Add the hash to
plugin.yamlunder thesha256field - Commit and push; CI will verify the hash remains correct
Existing plugins without a sha256 field are unaffected (verification is skipped with a warning log).
Draft — will submit via GitHub API when auth token recovers.