* 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>
57 lines
2.1 KiB
Python
57 lines
2.1 KiB
Python
"""Molecule AI remote-agent SDK — build agents that run outside the platform
|
||
network and register as first-class workspaces.
|
||
|
||
This is the Phase 30.8 companion to ``molecule_plugin`` (for plugin authors).
|
||
Where ``molecule_plugin`` helps you ship installable behavior for workspaces
|
||
that already exist, ``molecule_agent`` helps you *be* a workspace from the
|
||
other side of the wire: register, authenticate, pull secrets, heartbeat,
|
||
and detect pause/resume/delete — all via the Phase 30.1–30.5 HTTP contract.
|
||
|
||
Intended usage::
|
||
|
||
from molecule_agent import RemoteAgentClient
|
||
|
||
client = RemoteAgentClient(
|
||
workspace_id="550e8400-e29b-41d4-a716-446655440000",
|
||
platform_url="https://your-platform.example.com",
|
||
agent_card={"name": "my-remote-agent", "skills": []},
|
||
)
|
||
client.register() # mints + persists the auth token
|
||
env = client.pull_secrets() # decrypted secrets dict
|
||
client.run_heartbeat_loop() # background heartbeat + state-poll
|
||
|
||
See ``sdk/python/examples/remote-agent/`` for a runnable demo.
|
||
|
||
Design notes:
|
||
* **No async.** The SDK uses blocking ``requests`` so a remote agent author
|
||
can embed it in any event loop / thread / script without forcing anyio.
|
||
* **Token cached on disk** at ``~/.molecule/<workspace_id>/.auth_token``
|
||
with 0600 permissions, so a restart of the agent doesn't re-issue a
|
||
token (the platform refuses to issue a second token when one is on file).
|
||
* **Pause/delete detection is polling-based** because remote agents usually
|
||
can't expose an inbound WebSocket reachable from the platform.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from .client import (
|
||
PeerInfo,
|
||
RemoteAgentClient,
|
||
WorkspaceState,
|
||
verify_plugin_sha256,
|
||
)
|
||
|
||
# compute_plugin_sha256 lives in __main__ (the CLI entry point).
|
||
# Import it here so `from molecule_agent import compute_plugin_sha256` works.
|
||
from .__main__ import compute_plugin_sha256
|
||
|
||
__all__ = [
|
||
"RemoteAgentClient",
|
||
"WorkspaceState",
|
||
"PeerInfo",
|
||
"compute_plugin_sha256",
|
||
"verify_plugin_sha256",
|
||
"__version__",
|
||
]
|
||
__version__ = "0.1.0"
|