diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f603386 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "permissions": { + "allow": [ + "Bash(git *)", + "Bash(pip *)", + "Bash(python *)", + "Bash(pytest *)", + "Bash(python -m *)", + "Read", + "Glob", + "Grep" + ], + "deny": [ + "Bash(git push --force *)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "echo 'Bash executed'", + "once": true + } + ] + } + ] + }, + "cleanupPeriodDays": 30 +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..11cc05b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,210 @@ +# CLAUDE.md — molecule-sdk-python + +## Project overview + +Python SDK for the Molecule AI agent platform. Exposes two user-facing packages: + +- **`molecule_agent`** — Phase 30.8 remote-agent client. Write an agent that runs + outside the platform's Docker network; it registers with the platform, pulls + secrets, sends heartbeats, and detects pause/delete. Wraps the Phase 30.1–30.7 + HTTP contract (register, secrets, heartbeat, state-poll, A2A peer discovery, + delegation, plugin install). + +- **`molecule_plugin`** — Plugin-authoring SDK. Build installable plugin directories + that ship rules, skills (agentskills.io format), and per-runtime adaptors to any + Molecule AI workspace. Ships validators for plugin.yaml, SKILL.md (agentskills.io + spec), workspace/org/channel templates, and a `python -m molecule_plugin` CLI. + +Both packages are published together as `molecule-ai-sdk` on PyPI (`setuptools`, +`pyproject.toml`, `requires-python = ">=3.11"`). + +--- + +## Build and test + +```bash +# Install in dev mode +pip install -e . + +# Run the full suite +pytest + +# Run only molecule_agent tests (remote-agent client) +pytest tests/test_remote_agent.py + +# Run only molecule_plugin tests (SDK + validators) +pytest tests/test_sdk.py tests/test_validators.py + +# CLI smoke +python -m molecule_plugin validate --help +python -m molecule_plugin validate plugin /path/to/my-plugin/ +python -m molecule_plugin validate workspace /path/to/workspace-template/ +``` + +Tests use standard `pytest` fixtures with in-memory mocks — no live platform +required. The `molecule_agent` tests mock `requests.Session` directly via +`unittest.mock.MagicMock`. + +--- + +## Package conventions + +``` +molecule_agent/ # Remote-agent client (blocking requests, Phase 30) + client.py # RemoteAgentClient, WorkspaceState, PeerInfo, + # make_idempotency_key, _safe_extract_tar + +molecule_plugin/ # Plugin-authoring SDK + protocol.py # PluginAdaptor (runtime_checkable Protocol), + # InstallContext, InstallResult + builtins.py # AgentskillsAdaptor (default), + # SKIP_ROOT_MD, _install_claude_layer + manifest.py # PLUGIN_YAML_SCHEMA, validate_manifest, + # parse_skill_md, validate_skill, validate_plugin + workspace.py # validate_workspace_template, SUPPORTED_RUNTIMES + org.py # validate_org_template + channel.py # validate_channel_config, validate_channel_file, + # SUPPORTED_CHANNEL_TYPES + __main__.py # CLI: python -m molecule_plugin validate [plugin|workspace|org|channel] + +template/ # Reference plugin layout (NOT pip-installable) + adapters/ + claude_code.py # AgentskillsAdaptor — one-liner per runtime + deepagents.py # AgentskillsAdaptor — one-liner per runtime + +examples/remote-agent/ # Runnable Phase 30.1–30.5 demo + run.py +``` + +### Adding a new tool or endpoint to molecule_agent + +1. Pick the Phase 30 sub-phase that matches the contract (e.g. 30.6 = peer + discovery). +2. Add the method to `RemoteAgentClient` in `client.py`. Follow the existing + pattern: `_auth_headers()` for bearer token, `raise_for_status()` on the + response, `logger.warning()` instead of re-raising for transient errors in + loops. +3. Add a corresponding test fixture + test cases in `tests/test_remote_agent.py`. + Mock `client._session.get/.post` with `FakeResponse` or a `side_effect`. +4. Export from `__init__.py` and add to `__all__`. + +### Adding a new validator to molecule_plugin + +1. Add the validation function to the appropriate module (`manifest.py` for + SKILL.md, `workspace.py` for workspace templates, etc.). +2. Return a list of error strings (manifest layer) or a list of + `ValidationError` objects (workspace/org/channel layer — see existing + patterns in `workspace.py`). +3. Re-export from `molecule_plugin/__init__.py`. +4. Add `python -m molecule_plugin validate /path` CLI cases or hook into + the existing dispatch in `__main__.py` if the kind is new. +5. Add tests in `tests/test_sdk.py` or `tests/test_validators.py`. + +--- + +## Release process + +PyPI publication is automated via GitHub Actions and triggered by **git tags** with +a `v` prefix matching the version in `pyproject.toml` (e.g. tag `v0.2.1` publishes +`molecule-ai-sdk==0.2.1`): + +```bash +# 1. Update version in pyproject.toml +# 2. Tag and push +git tag v0.2.1 +git push origin v0.2.1 +``` + +The GitHub Actions workflow handles sdist + wheel build and upload to PyPI. +No manual steps required. Ensure you have PyPI token permissions in the repo +secrets before the first release. + +--- + +## Platform integration notes + +`molecule_agent` wraps these Phase 30 HTTP endpoints (all require bearer token +unless noted): + +| Method | Endpoint | Phase | Auth | +|--------|----------|-------|------| +| `POST` | `/registry/register` | 30.1 | none (issues token) | +| `GET` | `/workspaces/:id/secrets/values` | 30.2 | bearer | +| `POST` | `/registry/heartbeat` | 30.1 | bearer | +| `GET` | `/workspaces/:id/state` | 30.4 | bearer | +| `GET` | `/registry/:id/peers` | 30.6 | bearer + X-Workspace-ID | +| `GET` | `/registry/discover/:id` | 30.6 | bearer + X-Workspace-ID | +| `POST` | peer direct URL (A2A) | 30.6 | bearer + X-Workspace-ID | +| `POST` | `/workspaces/:id/a2a` (proxy) | 30.6 | bearer + X-Workspace-ID | +| `POST` | `/workspaces/:id/delegate` | 30.6 | bearer + X-Workspace-ID, 300s timeout | +| `GET` | `/workspaces/:id/plugins/:name/download` | 30.3 | bearer | +| `POST` | `/workspaces/:id/plugins` | 30.3 | bearer | + +**Token** is cached at `~/.molecule//.auth_token` with `0600` +permissions. On restart the client reuses the cached token — the platform +refuses to issue a second token when one is on file. + +**Idempotency (KI-002):** `delegate()` auto-generates an idempotency key as +`SHA256(task + current_minute)` (rounded to the minute). Two container restarts +within the same minute that send the same task string share the key, preventing +duplicate processing. + +**Plugin install:** Tars are extracted with `_safe_extract_tar()` — rejects +`..` path components and absolute paths; silently skips symlinks/hardlinks. +Atomic rename via staging dir + rename prevents partial installs. + +--- + +## SDK-specific conventions + +- **Python:** `>=3.11`, no external async dependencies in `molecule_agent` + (uses blocking `requests` so it embeds in any event loop). `molecule_plugin` + adaptor methods are `async` (`install`/`uninstall` satisfy `PluginAdaptor`). + +- **Async:** `molecule_plugin` uses `async def`/`await` for `PluginAdaptor`. + Call `asyncio.run(adaptor.install(ctx))` to run inline in a sync context. + +- **Error handling:** Network errors in loops are logged and swallowed so a + transient platform hiccup does not take a remote agent offline. API-level + errors (4xx) propagate via `raise_for_status()`. + +- **Token security:** Token file created with `0o600` — other local users must + not be able to read it. `_safe_extract_tar` guards against tar-slip attacks + in plugin install. + +- **Validation:** `validate_manifest`/`validate_skill`/`validate_plugin` are + pure and have no external dependencies (no `jsonschema`). They return lists + of error strings. The workspace/org/channel validators return + `list[ValidationError]` objects with `.file` and `.message` fields. + +- **First-party plugins:** `test_first_party_plugins_are_spec_compliant()` in + `tests/test_sdk.py` validates every plugin in the repo's top-level `plugins/` + directory against full agentskills.io spec. Keep that test passing. + +--- + +## Known issues + +- Before patching a silent failure or quirky behavior, **file a GitHub issue + first**. Do not patch silently — the SDK is consumed across multiple + runtime environments and silent patches can cause subtle breakage elsewhere. + +- `molecule_agent` does not yet bundle an inbound A2A server helper. + Platform-initiated calls to a remote agent without a publicly reachable + endpoint will not succeed. See Phase 30.8b in the platform's `PLAN.md`. + +--- + +## Relevant platform docs + +- **Platform conventions:** `docs/development/constraints-and-rules.md` — no auth + for MVP, Postgres as source of truth, no secrets in bundles, generic + workspace-template. +- **Secrets runbook:** `docs/runbooks/saas-secrets.md` — read before rotating any + secrets. +- **Cron learnings:** `cron-learnings.md` (platform root) — read before reviewing + PRs; write a 1-line reflection to `.claude/per-tick-reflections.md` after + triage. +- **CLAUDE.md/PLAN.md sync PRs:** treat these as always noteworthy. +- **molecule-core docs:** Full platform `PLAN.md` and architecture docs at + `https://github.com/hongmingw/molecule-monorepo` diff --git a/known-issues.md b/known-issues.md new file mode 100644 index 0000000..947947a --- /dev/null +++ b/known-issues.md @@ -0,0 +1,155 @@ +# Known Issues — molecule-sdk-python + +Issues identified in source but not yet filed as GitHub issues (GH_TOKEN +unavailable in automated agent contexts). Each entry has: location, +symptom, impact, suggested fix. + +Format per entry: +``` +## KI-N — Short title + +**File:** `:` +**Status:** TODO comment / identified / partially fixed +**Severity:** Critical / High / Medium / Low +**Platform phase:** (optional — which Phase 30 sub-phase is affected) + +### Symptom +... + +### Impact +... + +### Suggested fix +... +--- +``` + +--- + +## KI-001 — RemoteAgentClient does not implement inbound A2A server + +**File:** `molecule_agent/client.py` +**Status:** Known limitation; not yet implemented +**Severity:** Medium +**Platform phase:** Phase 30.8b + +### Symptom +`RemoteAgentClient` can call other workspaces via A2A (outbound), but cannot +receive inbound A2A calls. Any workspace that tries to delegate to or message +this agent will get a connection refused or timeout. + +### Impact +Agents running outside the platform's Docker network via `molecule_agent` are +one-directional. Platform agents cannot push work to them — the remote agent +must poll or be provisioned with a publicly reachable webhook endpoint. + +### Suggested fix +Add an `A2AServerMixin` class that exposes a `FastAPI` or `flask` route +(`POST /a2a/inbound`) and runs in a background thread alongside the client's +heartbeat loop. Register the inbound URL with the platform via the +`/registry/discover` update endpoint when the server starts. See Phase 30.8b +in the platform `PLAN.md`. + +--- + +## KI-002 — Delegation has no server-side idempotency key enforcement + +**File:** `molecule_agent/client.py` (client-side SHA256 key) +**Status:** Partially mitigated client-side (SHA256 rounded-to-minute) +**Severity:** Medium +**Platform phase:** Phase 30.6 + +### Symptom +The client generates an idempotency key as `SHA256(task + current_minute)`, but +the platform's `POST /workspaces/:id/delegate` endpoint does not enforce +idempotency server-side. Two identical tasks sent within the same calendar +minute produce duplicate processing if the platform accepts both. + +### Impact +A workspace container restart mid-delegation (e.g. liveness probe restart) that +fires the same delegation request twice will result in duplicate side-effects +(double commits, double API calls, double messages) if the platform has not yet +stored the first delegation's result. + +### Suggested fix +Platform-side: accept an optional `idempotency_key` field in +`POST /workspaces/:id/delegate`, check for existing non-failed delegation with +the same `(workspace_id, idempotency_key)`, return HTTP 200 with existing ID +instead of creating a new row. Client-side key generation is correct; it is +the server that needs to honor it. + +--- + +## KI-003 — `_safe_extract_tar` silently skips all symlinks + +**File:** `molecule_agent/client.py:_safe_extract_tar` +**Status:** By design (security posture) +**Severity:** Low (misleading behavior) + +### Symptom +When extracting plugin tarballs, `_safe_extract_tar` silently skips any entry +that is a symlink. This means plugin tarballs that legitimately use symlinks +for shared assets (e.g., `assets/logo.png -> ../shared/logo.png`) will be +silently omitted from the extracted plugin directory with no error or warning. + +### Impact +Some valid plugins may appear to install successfully but be missing files at +runtime. This can manifest as confusing "file not found" errors that are hard to +trace to the install step. + +### Suggested fix +Emit a `logger.warning()` for each skipped symlink so operators can see what +was dropped. Alternatively, allow safe relative symlinks (those resolving +within the extraction root) while blocking absolute symlinks and `..`-escaping +symlinks. Document the behavior in the plugin authoring guide. + +--- + +## KI-004 — Token file races between concurrent instances of RemoteAgentClient + +**File:** `molecule_agent/client.py` (token caching) +**Status:** Identified +**Severity:** Low + +### Symptom +Multiple `RemoteAgentClient` instances sharing the same `workspace_id` write to +the same token cache file (`~/.molecule//.auth_token`). If two +instances start simultaneously, the file read/write is not atomic — one +instance may read a partially-written token or overwrite a valid token with an +older one. + +### Impact +On a cold start with multiple workers for the same workspace, some workers may +fail to register because their token is stale. The platform refuses to issue a +second token when one exists on disk. + +### Suggested fix +Use a file-based lock (e.g. `fcntl.flock` or `portalocker`) around token read +and write operations. Alternatively, use per-process token storage (in-memory) +and only write to disk as a recovery fallback. + +--- + +## KI-005 — `validate_plugin` does not check for secrets in bundle manifests + +**File:** `molecule_plugin/manifest.py:validate_manifest` +**Status:** Not yet implemented +**Severity:** High + +### Symptom +`validate_manifest` does not scan the `env:` or `secrets:` fields of a +`plugin.yaml` for hardcoded credentials (API keys, passwords, tokens). Plugin +authors could accidentally commit secrets into what should be a generic bundle. + +### Impact +Secrets committed to a plugin manifest are visible in the repo and any tarball +published to PyPI or the plugin registry. Per platform constraints +(`constraints-and-rules.md`), bundles must never contain secrets. + +### Suggested fix +Add a `validate_no_secrets()` check in `validate_manifest` that scans all +string values in the manifest for patterns matching common secret formats +(`sk-`, `ghp_`, ` Bearer `, 32+ char hex strings, etc.). Return a +`ValidationError` with level `HIGH` if any are found, even in example or +placeholder values. Add a corresponding test with a manifest containing a +known secret pattern.