Metadata-Version: 2.4
Name: molecule-ai-sdk
Version: 0.6.1
Summary: Molecule AI SDK — build plugins (molecule_plugin) AND remote agents that join a Molecule AI org from another machine (molecule_external_workspace).
Author: Molecule AI
License: MIT
Project-URL: Homepage, https://git.moleculesai.app/molecule-ai/molecule-ai-sdk
Project-URL: Repository, https://git.moleculesai.app/molecule-ai/molecule-ai-sdk
Project-URL: Documentation, https://git.moleculesai.app/molecule-ai/molecule-ai-sdk#readme
Keywords: agents,ai,multi-agent,a2a,plugins,saas,remote-agent
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.31
Provides-Extra: channel
Requires-Dist: httpx<1,>=0.27; extra == "channel"
Provides-Extra: test
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
Requires-Dist: jsonschema>=4.18; extra == "test"
Requires-Dist: build>=1.2; extra == "test"
Requires-Dist: setuptools==83.0.0; extra == "test"
Requires-Dist: wheel>=0.43; extra == "test"
Requires-Dist: httpx<1,>=0.27; extra == "test"
Provides-Extra: contracts
Requires-Dist: molecule-ai-contracts>=0.1.0; extra == "contracts"
Dynamic: license-file

# molecule-ai-sdk

The **Molecule AI SDK and contract SSOT**. One repo, three things:

1. **Two Python packages** (published together under the distribution name
   `molecule-ai-sdk` in Molecule's publicly readable Gitea package registry;
   publishing remains authenticated, and the package is **not** on public PyPI):
   - **`molecule_external_workspace`** — write an agent that runs *outside* the
     platform's Docker network and joins a Molecule AI org from another machine.
     It registers with the platform, pulls secrets, sends heartbeats, discovers
     peers (A2A), delegates, and detects pause/delete. Public API:
     `RemoteAgentClient`, `A2AServer`.
   - **`molecule_plugin`** — build installable **plugin** directories (rules,
     skills in agentskills.io format, per-runtime install adaptors) + validators
     and the `molecule-plugin` CLI. (See [Plugin authoring](#plugin-authoring--molecule_plugin).)

2. **`contracts/` — the platform contract SSOT.** The JSON-Schema (draft 2020-12)
   IDL for every cross-boundary contract in the platform, plus the cloud-provider
   YAML SSOT. This is the former `molecule-contracts` repo, folded in and archived
   here 2026-07-01. See [Contracts](#contracts--the-ssot).

3. **`gen/` — generated bindings.** Go / TypeScript / Python types + constants
   emitted from `contracts/` by `tools/gen-*.mjs`. Consumed by `molecule-core`
   and `molecule-controlplane` (Go, via `go.moleculesai.app/sdk/gen/go/...`),
   the storefront (TS), and the runtime (Python). **Never hand-edited** — a
   fresh regen must match what's committed (enforced by CI).

```
molecule-ai-sdk/
├── molecule_external_workspace/   # remote-agent client (RemoteAgentClient / A2AServer)
├── molecule_plugin/               # plugin-authoring SDK + validators + CLI
├── contracts/                     # JSON-Schema IDL SSOT + cloudproviders.yaml
│   ├── mcp/  plugin-manifest/  workspace-template/  org-template/
│   ├── catalog/                   # catalog-entry + marketplace-service (catalog/publish/install/entitlement)
│   ├── provision-request/  promote-request/  workspace-comms/
│   └── cloudproviders.yaml + cloudproviders.schema.json
├── gen/{go,ts,python}/            # generated bindings (DO NOT EDIT)
├── tools/gen-runtimes.mjs + gen-{go,ts,python}.mjs  # Node generators
├── molecule_plugin/templates/     # packaged skill/MCP/channel/trigger scaffolds
└── tests/
```

## SDK release boundary: 0.6.1 is published

`sdk-v0.6.1` is tagged and `molecule-ai-sdk==0.6.1` is published to the Molecule
Gitea registry (2026-08-05). It is the latest published version and the one the
install example below pins. Everything documented on this branch — including the
native-channel retirement validator described further down — is in that wheel;
an install from the registry now *does* prove the 0.6.0 channel cutover.

0.6.1 is a hotfix over 0.6.0 and carries no breaking change of its own. It fixes
the `kind: trigger` delivery watchdog cancelling every delivery on a workspace
that was idle before the fire started: the runtime turn lease is
workspace-global, so on a workspace quiet past the 900s idle TTL it was already
`idle_expired` when the delivery began, and the first 30s probe killed it into a
permanent cancel/retry loop. A lease is now honoured only when it was armed
after the delivery it is being read about. See [`CHANGELOG.md`](CHANGELOG.md).

0.6.0 is a **breaking** release: the native org-template `channels` retirement,
the `local`/`docker` → `enteros` substrate rename, the Tier-1 branding flip, the
LLM-registry platform-arm correction, the removal of
`MOLECULE_TRIGGER_DELIVERY_WATCHDOG_SECONDS`, and `A2AServer` fail-closed
inbound auth. Read the *Breaking* section of [`CHANGELOG.md`](CHANGELOG.md)
before upgrading — the substrate rename in particular has a **data
precondition** (molecule-controlplane migrations 070 and 071) that upgrading the
SDK alone does not satisfy.

Contributors validating an *unreleased* candidate should use a reviewed, exact
source commit and install that checkout with `python -m pip install -e '.[test]'`.
Production consumers should stay on an immutable published version.

## Install

```bash
SDK_DOWNLOAD="$(mktemp -d)"
python -m pip download --no-deps --dest "$SDK_DOWNLOAD" \
  --index-url "https://git.moleculesai.app/api/packages/molecule-ai/pypi/simple" \
  molecule-ai-sdk==0.6.1
python -m pip install --index-url https://pypi.org/simple \
  "$SDK_DOWNLOAD"/molecule_ai_sdk-*.whl
```

The Gitea registry does not proxy public PyPI. Download only the private SDK
wheel from Gitea, then install that local wheel while resolving its declared
public dependencies from PyPI. Keeping retrieval in separate commands avoids
both a broken sole-index install, dependency drift, and cross-index dependency
confusion. Authenticate via `~/.netrc` (or pip's keyring) — do not inline
credentials in the URL.

Go consumers import the generated bindings via the vanity path (no PyPI needed):

```go
import molcontracts "go.moleculesai.app/sdk/gen/go/molcontracts"
```

## Contracts — the SSOT

Every contract is a **pair** under its `contracts/<domain>/` directory:

- **`*.schema.json`** — the rules (JSON-Schema draft 2020-12): fields, types,
  `required`, `enum`, `const` pins, `pattern`s. The enforceable definition.
- **`*.contract.json`** — one canonical **instance** that MUST validate against
  its sibling schema. It's the worked example, the CI anchor, and — for
  value-bearing contracts like `mcp-plugin-delivery` — the SSOT for the concrete
  values baked into `gen/`.

Direction: `*.contract.json ──validate──▶ *.schema.json`, and
`contracts/ ──codegen (tools/gen-*.mjs)──▶ gen/{go,ts,python}`. Each domain
directory under [`contracts/`](./contracts) carries its own `README.md`
describing that surface.

Eight jobs back this under the strict `all-required` aggregator in
`.gitea/workflows/contracts-codegen-drift.yml`: schema validation, codegen drift,
cloud-provider and adapter/prompt/platform conformance, Go parity, and published
binding parity. Regenerate locally after any contract edit and commit the result:

```bash
node tools/gen-runtimes.mjs
node tools/gen-go.mjs && node tools/gen-ts.mjs && node tools/gen-python.mjs
```

## Remote agents — `molecule_external_workspace`

```python
from molecule_external_workspace import RemoteAgentClient

client = RemoteAgentClient(
    platform_url="https://<org>.moleculesai.app",
    workspace_id="my-agent",
    # the auth token is minted on first register() and persisted client-side
)
```

The client wraps the workspace↔platform HTTP contract (register, pull secrets,
heartbeat, state-poll, A2A proxy delivery, delegation, plugin install) — the same
surface captured in `contracts/workspace-comms/`. `A2AServer` is the authenticated
receive side for agent-to-agent messages and rejects traffic until registration
provides a `platform_inbound_secret`. Before exposing it publicly, attach it to the
client, register, require the returned secret, and only then start the listener.

## Plugin authoring — `molecule_plugin`

A Molecule AI plugin is a directory that bundles rules, skills, and per-runtime
install adaptors. Any plugin that conforms to the contract is installable on any
Molecule AI workspace whose runtime the plugin supports.

### Quick start

Create a validated scaffold directly from the installed wheel:

```bash
molecule-plugin init my-plugin --kind skill
cd my-plugin
molecule-plugin validate plugin .
```

Use `--kind mcp` for a dependency-free stdio MCP starter, `--kind channel`
for a supervised provider bridge with a vendored, provider-neutral channel API
v1 client, or `--kind trigger` for an autonomous scheduler. Names must be
lowercase alphanumeric slugs with single hyphens. The initializer refuses path
traversal and never overwrites an existing directory.

```
my-plugin/
├── .gitea/workflows/ci.yml  # generated repository CI
├── .gitignore
├── README.md
├── plugin.yaml              # name, version, runtimes, description
├── adapters/
│   ├── claude_code.py       # AgentskillsAdaptor entrypoint
│   └── codex.py
├── skills/my-plugin/
│   └── SKILL.md             # instructions injected into the system prompt
└── tests/test_scaffold.py   # generated scaffold smoke test
```

Validate:

```python
from molecule_plugin import validate_manifest
errors = validate_manifest("my-plugin/plugin.yaml")
assert not errors, errors
```

### CLI

```bash
molecule-plugin init my-skill-plugin --kind skill
molecule-plugin init my-mcp-plugin   --kind mcp
molecule-plugin init my-channel      --kind channel
molecule-plugin init my-scheduler    --kind trigger
molecule-plugin validate plugin    my-plugin/
molecule-plugin validate workspace workspace-configs-templates/claude-code-default/
molecule-plugin validate org       org-templates/molecule-dev/
molecule-plugin validate my-plugin/   # kind defaults to 'plugin'

# Channel and trigger repos pin a vendored copy of the public local-A2A client.
molecule-plugin channel-sdk check my-channel/
molecule-plugin channel-sdk sync  my-channel/  # intentional SDK upgrade
```

Exit code is 0 when valid, 1 when any errors are found — suitable for CI.
Add `-q` / `--quiet` to suppress success lines and emit only errors.
`python -m molecule_plugin ...` remains an equivalent invocation.

Manifest validation follows the canonical
`contracts/plugin-manifest/plugin-manifest.schema.json`: `name`, `version`, and
`description` are required; known `contributes` entries are shape-checked;
unknown contribution points remain forward-compatible. The authoring validator
requires every declared daemon to match the canonical `name`/`command`/`args`/
`env`/`cwd` shape, so a typo fails before publication. Runtime admission remains
backward-compatible: an already-installed legacy manifest with a malformed
daemon is skipped with a warning instead of bricking the workspace.

The following channel-cutover behavior shipped in 0.6.0 and is present in the
published `molecule-ai-sdk==0.6.0` package. It is **not** in 0.5.5 or in the
`release/0.5.x` line, so a consumer still on either must treat it as a breaking
upgrade — see the *Breaking* section of [`CHANGELOG.md`](CHANGELOG.md).

Channel integrations are `kind: channel` plugins in `plugins[]`; the retired
native org-template `channels` key is rejected at the root, defaults, and every
recursive workspace node with migration guidance. The schema and authoring
validator match the reserved `channels`/`defaults`/`workspaces`/`children`
field names case-insensitively in YAML and JSON-form documents. Validation also
inspects every explicit duplicate structural-key occurrence before normal
last-key-wins decoding, while preserving explicit-key precedence over YAML
merge sources. Nested business mappings such as `category_routing.channels`
remain valid. The public
`molecule_plugin.channel` module owns the API v1 client contract:
reserved runtime environment names, canonical A2A request construction,
response parsing, and explicit unavailable/protocol/delivery-unknown failures.
Generated channel scaffolds vendor that module byte-for-byte so their daemon
does not import private runtime code. Generated pull-request CI validates the
manifest, checks that the vendored client still matches its pinned SDK, and
runs the scaffold tests. The SDK's own required PR suite also exercises a
provider event through the generated bridge and a real authenticated Unix
socket to an agent reply.

Programmatic equivalents:

```python
from molecule_plugin import (
    send_channel_message,
    channel_message_response_text,
    validate_plugin,
    validate_workspace_template,
    validate_org_template,
)
```

### Per-runtime adaptors — when to write a custom one

The default `AgentskillsAdaptor` handles the common shape: rules go into the
runtime's memory file (CLAUDE.md), skill dirs go into `/configs/skills/`. That
covers most plugins.

The adaptor records exact ownership under
`/configs/.molecule/plugin-ownership/`. Uninstall removes only unchanged files,
the exact marked memory block, and the plugin's exact settings contribution;
pre-existing and user-modified content is preserved. Legacy installs without a
record are a safe no-op. See
[`docs/migrations/plugin-uninstall-ownership.md`](./docs/migrations/plugin-uninstall-ownership.md).

Write a custom adaptor when you need to:

- **Register runtime tools dynamically** — call `ctx.register_tool(name, fn)`.
- **Register runtime sub-agents** — call `ctx.register_subagent(name, spec)`.
- **Write to a non-standard memory file** — call `ctx.append_to_memory(filename, content)`.

Minimum custom adaptor:

```python
# adapters/codex.py
from molecule_plugin import InstallContext, InstallResult

class Adaptor:
    def __init__(self, plugin_name: str, runtime: str):
        self.plugin_name, self.runtime = plugin_name, runtime

    async def install(self, ctx: InstallContext) -> InstallResult:
        ctx.register_subagent("my-agent", {"prompt": "...", "tools": [...]})
        return InstallResult(plugin_name=self.plugin_name, runtime=self.runtime, source="plugin")

    async def uninstall(self, ctx: InstallContext) -> None:
        pass
```

### Resolution order (understood by the platform)

For `(plugin_name, runtime)`:

1. **Platform registry** — `workspace-template/plugins_registry/<plugin>/<runtime>.py`
   (curated; set by the Molecule AI team for quality-assured plugins).
2. **Plugin-shipped** — `<plugin_root>/adapters/<runtime>.py` (what this SDK helps you build).
3. **Raw-drop fallback** — copies plugin files into `/configs/plugins/<name>/`
   and surfaces a warning; no tools are wired.

You generally ship for path #2. If your plugin becomes popular enough to be
promoted to "default," the Molecule AI team PRs a copy of your adaptor into the
platform registry (path #1) so it survives upstream breakage.

### Runtime identifiers and official support

Runtime IDs are open, bounded, path-safe slugs defined by
`contracts/adapter/runtime-id.schema.json`; third-party adapters are not blocked
by a first-party allowlist. The separate official registry contains Claude Code,
Codex, Hermes, and OpenClaw. Known aliases such as `claude_code` normalize to
their canonical ID, while other valid IDs remain unchanged.

## Build and test

```bash
pip install -e '.[test]'   # base packages + pytest-asyncio
pytest -q
```
