The heartbeat loop runs unbounded with no way for an external caller
(SIGTERM handler, MCP client disconnect) to signal it to exit cleanly.
This causes orphaned heartbeat API calls after the controlling client
has disconnected.
Suggested fix: add stop_event parameter (threading.Event) to
run_heartbeat_loop() so callers can achieve clean shutdown.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GitHub org Molecule-AI was suspended 2026-05-06; SCM moved to Gitea
(git.moleculesai.app). The wholesale `git push --mirror` migration left
workflow files under .github/workflows/, which Gitea Actions does NOT
read - it reads .gitea/workflows/ exclusively.
This rename + the cross-repo `uses:` path rewrite are the minimum
edits to make CI fire on this repo again. The workflow content itself
is not modified (other than the path rewrites and lowercasing of the
old `Molecule-AI` org reference to the post-suspension `molecule-ai`).
Refs: feedback_post_suspension_migration_must_sweep_dormant_repos
Platform now wraps peer A2A responses in [A2A_RESULT_FROM_PEER]...
[/A2A_RESULT_FROM_PEER] markers (OFFSEC-003) to mark them as untrusted
third-party content. This change adds:
- strip_a2a_boundary(text): strips the wrapper and returns the interior
content. Safe on pre-OFFSEC-003 responses (returns input unchanged when
markers absent or malformed) and on None/empty.
Exported from molecule_agent/__init__.py and added to __all__.
README updated with a dedicated OFFSEC-003 section and call_peer() table
note pointing to strip_a2a_boundary().
8 new tests: basic, whitespace edges, no-markers passthrough, only-start,
only-end, empty/None, end-before-start edge case, multiline content.
305 total tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds optional org_id and origin constructor kwargs that inject
X-Molecule-Org-Id and Origin headers on every request, enabling
SDK use against multi-tenant SaaS deployments (*.moleculesai.app)
without needing a pre-configured requests.Session.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Exposes the two platform-supported filter parameters on
RemoteAgentClient.fetch_inbound():
- peer_id: narrow inbound events to a specific peer workspace UUID
- before_ts: RFC3339 cutoff timestamp for historical backlog replay
Also marks the corresponding README limitation as resolved (was documented as
"does not expose peer_id or before_ts filters" — both are now wired up).
New tests:
- test_fetch_inbound_peer_id_filter
- test_fetch_inbound_before_ts_filter
- test_fetch_inbound_combined_filters
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updates InboundMessage to surface the three channel-envelope enrichment
fields (peer_name, peer_role, agent_card_url) as typed attributes instead
of requiring callers to read them from msg.raw["data"]. Fields default to
"" when absent so existing callers are unaffected.
Also marks KI-001 (RemoteAgentClient does not implement inbound A2A server)
as resolved — A2AServer, PushDelivery, and PollDelivery all ship in the
current codebase; the known-issues.md entry was stale.
New tests:
- test_parse_activity_row_enrichment_fields
- test_parse_activity_row_enrichment_fields_absent
- test_parse_activity_row_enrichment_fields_null_becomes_empty
- test_parse_activity_row_enrichment_in_canvas_user_row
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[sdk-lead-agent] Merging — PR makes the optional test-extras install explicit and fixes silent skip of 4 async tests. CI green on 3.11/3.13. Closes#2 (duplicate).
Co-authored-by: Molecule AI Technical Writer <technical-writer@agents.moleculesai.app>
Co-committed-by: Molecule AI Technical Writer <technical-writer@agents.moleculesai.app>
The GitHub org Molecule-AI was suspended on 2026-05-06; canonical SCM
is now Gitea at https://git.moleculesai.app/molecule-ai/. Stale
github.com/Molecule-AI/... URLs return 404 and break tooling that
clones / pip-installs / curls them.
This bundles all non-Go-module URL fixes for this repo into a single PR.
Go module path references (in *.go, go.mod, go.sum) are out of scope
here -- tracked separately under Task #140.
Token-auth clone URLs also flip ${GITHUB_TOKEN} -> ${GITEA_TOKEN} since
the GitHub token does not auth against Gitea.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verify whether failure was setup-python toolcache class (now fixed via
orchestrator's runners-1-8 recreate) or real CODE class.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the docs gaps that were left when several CP changes landed without
SDK-side coverage:
- "What this is / what this isn't" callout — distinguishes molecule_agent
(outside-workspace client SDK) from molecule-ai-workspace-runtime
(in-workspace runtime), with strong forward-pointer to the runtime docs
- Channel envelope (wire format) — including the three enrichment fields
added 2026-05-02 (peer_name, peer_role, agent_card_url)
- A2A reply transport table — explicit /notify vs /a2a routing per source
- Limitations & roadmap — names the SDK gaps so follow-up issues/PRs
are trivial to file:
* fetch_inbound() missing peer_id + before_ts filters (CP PRs #2472, #2476)
* InboundMessage missing typed peer_name/peer_role/agent_card_url
* RemoteAgentClient does not auto-inject X-Molecule-Org-Id + Origin
(with a session-based workaround)
Docs-only — no Python code touched. Code work for the named gaps is
deferred to follow-up PRs so reviewers can land docs first.
The previous commit (87a4cfc) used `git add -A` and pulled in __pycache__/
and *.egg-info/ files because .gitignore didn't exclude them. Untrack
those, and extend .gitignore so future `git add -A` is safe.
This is a no-op for production code; only repo hygiene.
- Drop unused `import time` from inbound.py and `import call` from
test_inbound.py (caught by ruff in CI; would have caught locally if I'd
run it before pushing).
- Rewrite the misleading comment in PollDelivery.run_once: the cursor DOES
advance past handler exceptions (poison-pill resilience). The previous
comment claimed otherwise, which would have confused future readers.
- Drop `_parse_activity_row` from inbound.py's `__all__`. The leading
underscore signals "private helper"; exposing it via `__all__`
contradicted the convention. Tests still import it directly via the
module path.
- Add `test_fetch_inbound_429_retries_via_get_with_retry` — the PR
description claimed branch-coverage of the 429 path but no test
exercised it. Closes the gap.
External agents that can't expose a public HTTP endpoint (laptops behind
NAT, ephemeral CI runners, hermes self-hosted, codex et al) had to reverse-
engineer the activity-poll loop from molecule-mcp-claude-channel/server.ts
because the SDK only shipped the push-mode `A2AServer` (Phase 30.8b).
This adds the complementary path:
- `RemoteAgentClient.fetch_inbound(since_id=…)` — one-shot GET against
`/workspaces/:id/activity?type=a2a_receive&since_id=…`. Cursor-loss (410)
surfaces as `CursorLostError`; caller resets and re-polls.
- `RemoteAgentClient.reply(msg, text)` — smart-routes to `/notify` for
canvas users, `/a2a` (JSON-RPC envelope + X-Source-Workspace-Id) for peer
agents. Hides the reply-path bifurcation from connector authors.
- `PollDelivery` / `PushDelivery` / `InboundDelivery` protocol — same
`MessageHandler` callback works for both transports.
- `RemoteAgentClient.run_agent_loop(handler, delivery=None)` — combined
heartbeat + state-poll + inbound dispatch. Defaults to `PollDelivery`.
Async handlers detected and `asyncio.run`'d (matches A2AServer pattern).
Sleep cadence = min(heartbeat_interval, delivery.interval).
- `python -m molecule_agent connect` CLI — one-line bootstrap. Loads a
user's `module:function` via importlib, registers, runs the loop until
pause/delete or SIGTERM. All flags also read from environment variables.
Tests: 50 new (test_inbound.py, test_cli_connect.py) covering every prod
branch — source normalization, cursor advancement, 410 reset, async/sync
handler dispatch, handler exception → log+continue+advance, smart-reply
routing for canvas vs peer vs unknown sources, run_agent_loop terminal
states, sleep-interval selection, CLI handler resolution failures.
Resolves#17.
PyPI page for molecule-ai-sdk currently shows three broken links:
Homepage / Repository / Documentation →
github.com/hongmingw/molecule-monorepo (personal account, wrong repo name)
Real location is github.com/Molecule-AI/molecule-sdk-python (this repo).
Fix all three:
Homepage → github.com/Molecule-AI/molecule-sdk-python
Repository → github.com/Molecule-AI/molecule-sdk-python
Documentation → github.com/Molecule-AI/molecule-sdk-python#readme
Side note: I scanned the rest of the org for the same hongmingw/molecule-
monorepo pattern in pyproject.toml — only this package has it. No fix
needed elsewhere.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Adds molecule_agent.a2a_server.A2AServer — a bundled HTTP server that
receives inbound A2A calls so remote agents can receive work from the
platform without provisioning their own HTTP endpoint.
- A2AServer: threaded HTTPServer on POST /a2a/inbound
- Sync and async handlers both supported; async handlers run in a
dedicated event loop per call to avoid "no event loop in thread" errors
- 9 unit tests covering: lifecycle, routing, error handling, async path,
concurrent requests
- Exported from molecule_agent.__init__; client.py docstring updated
- Closes GitHub #14
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Triggers on push/PR to main. Runs tests across Python 3.11, 3.12, 3.13
and adds ruff linting as a separate step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PR accidentally embedded the new module-level constants and
make_idempotency_key function inside the __all__ list literal,
causing a SyntaxError. Fix closes the list after the existing exports
and adds make_idempotency_key + verify_plugin_sha256 as proper string
entries (compute_plugin_sha256 was removed in this PR so it is omitted).
KI-002: prevent duplicate delegation execution when a remote agent
container restarts mid-delegation. Remote agent clients now compute a
SHA-256 key from task_text + current wall-clock minute and POST it as
idempotency_key to /workspaces/:id/delegate.
The platform deduplicates requests sharing the same key within the
minute window. Callers can also pass an explicit idempotency_key to
override the auto-computed value.
New:
- make_idempotency_key(task_text) → str
- RemoteAgentClient.delegate(task, target_id, idempotency_key=None, timeout=300.0)
- 8 new unit tests covering headers, errors, explicit key override, and
make_idempotency_key invariants
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_symlink entries in plugin tarballs are skipped (security posture, correct) but
now emit a logger.warning so operators can audit what was dropped:
"skipping symlink in plugin tarball (not supported for security): <name> -> <target>"
Added test_safe_extract_logs_warning_for_skipped_symlink asserting the warning
is present in caplog records at WARNING level. All 211 tests pass (+1 new).
known-issues.md updated.
Add fcntl.flock around token read/write in load_token() and save_token():
- load_token(): shared lock (LOCK_SH | LOCK_NB) before reading.
Returns None if lock is contended rather than blocking.
- save_token(): exclusive lock (LOCK_EX | LOCK_NB) before writing.
Gracefully degrades (logs warning, skips write) if another writer
holds the lock; in-memory _token is still updated so this instance
functions correctly. Releases lock in finally block.
Concurrent readers are safe. Concurrent writers are serialised. The
platform's one-token-per-workspace invariant is preserved.
known-issues.md updated.
KI-007 (High): Add isinstance(value, str) guard to _is_hex() so
non-string arguments return False cleanly instead of raising TypeError.
Updated test_is_hex_non_string to assert False instead of expecting
pytest.raises(TypeError).
KI-005 (High): Add _scan_for_secrets() to manifest.py that walks all
string values in plugin.yaml and reports common credential patterns
(sk-, ghp_, AKIA, bearer tokens, long hex strings, password/api_key
assignments). Call it from validate_manifest(). Skips the sha256
field since it's a content-addressed hash, not a secret.
Run: pytest → 210 passed, 1 skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
* 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>
* 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
---------
Co-authored-by: Molecule AI SDK-Dev <sdk-dev@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>