Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ categories:
labels: [security]
- title: 'Chores'
labels: [chore, ci, dependencies]
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-template: '- $TITLE (#$NUMBER)'
change-title-escapes: '\<*_&'
version-resolver:
major:
Expand Down
34 changes: 17 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ jobs:
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[testing]" pytest pytest-cov jsonschema
# [cli] brings typer + jinja2 so the CLI tests run; fastapi + httpx
# are needed by the @route/build_router tests (FastAPI TestClient) and
# the with_tool example.
pip install -e ".[cli,testing]" pytest pytest-cov jsonschema fastapi httpx
- name: Lint (ruff)
run: |
pip install ruff
Expand All @@ -43,7 +46,7 @@ jobs:
run: pytest -v --cov=dryade_plugins_sdk --cov-report=xml -m "not e2e"

defense_in_depth:
name: Defense-in-depth (D-05, Rule §9, schema)
name: Defense-in-depth (contract integrity)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -55,15 +58,15 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -e ".[testing]" pytest jsonschema
- name: D-05 — zero core.* imports
- name: Zero host-runtime imports
run: pytest tests/test_zero_core_imports.py -v
- name: Rule §9 — hash conformance (SHA-256 + SHA3-256 byte-identical)
- name: Hash conformance (SHA-256 + SHA3-256)
run: pytest tests/test_hash_conformance.py -v
- name: Rule §11 — manifest schema rejects community tier
- name: Manifest schema rejects community tier
run: pytest tests/test_manifest_schema_sdk.py -v

brand_leak:
name: Brand-leak guard (T-09-1)
name: Internal-reference leak guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -75,11 +78,11 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install pytest
- name: Run brand-leak regression net
run: pytest tests/test_no_internal_leaks_in_marketing.py -v
- name: Run internal-reference leak guard
run: pytest tests/test_no_internal_leaks.py -v

examples_build:
name: Examples build (T-09-3)
name: Examples build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -90,7 +93,9 @@ jobs:
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[testing]" pytest jsonschema
# The with_tool example wires @route into FastAPI, so it needs the
# CLI extras + fastapi + httpx to build and pass its own test suite.
pip install -e ".[cli,testing]" pytest jsonschema fastapi httpx
- name: Build + test every example plugin
run: pytest tests/test_examples_build.py -v

Expand All @@ -107,12 +112,7 @@ jobs:
- name: Install SDK + CLI from local source (hermetic — no PyPI dep)
run: |
python -m pip install --upgrade pip
pip install -e ".[testing]" pytest jsonschema
# The CLI lives in a separate repo. For SDK-repo CI we install the
# latest published `dryade-cli` from PyPI (after first release lands).
# Until then, fall back to the main branch of the CLI repo.
pip install dryade-cli || \
pip install "git+https://github.com/DryadeAI/dryade-cli.git@main"
pip install -e ".[cli,testing]" pytest jsonschema
- name: Run E2E smoke
run: pytest tests/test_smoke_e2e.py -v -m e2e

Expand All @@ -132,7 +132,7 @@ jobs:
- name: Verify license metadata in wheel
run: |
unzip -p dist/*.whl '*/METADATA' | grep -E '^(License|License-Expression)'
unzip -p dist/*.whl '*/METADATA' | grep -q 'LicenseRef-DSUL'
unzip -p dist/*.whl '*/METADATA' | grep -qE 'License(-Expression)?:\s*MIT'
- uses: actions/upload-artifact@v4
with:
name: dist
Expand Down
14 changes: 7 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ _release-drafter manages this section on every PR merge — do not edit by hand.
- (host, via DryadeAI/Dryade #962) The plugin loader's `isinstance`
load gate now accepts both the host's `PluginProtocol` ABC and the
public SDK's `Plugin` Protocol. Plugins authored with
`dryade plugin new` no longer need to inherit from `core.ee.*`.
`dryade plugin new` no longer need to inherit from a host base class.

## [1.0.0] — TBD

Expand All @@ -52,15 +52,15 @@ plugin authors install from PyPI.
bundled v2 JSON schema (`dryade-manifest-v2.schema.json`).
- **Packaging primitives** — `compute_plugin_hash_pair`, `sign_manifest`,
`load_private_key`, `verify_plugin_hash`, `CONTRACT_VERSION` constant
(set to 4 — SHA-256 + SHA3-256 dual hash, Rule §9).
(set to 4 — SHA-256 + SHA3-256 dual hash).
- **Hook decorators** — `@traced`, `@hook` as no-op shims the host wraps
with observability at load time.
- **Exception hierarchy** — `DryadePluginError`, `PluginValidationError`,
`ManifestValidationError`, `AgentExecutionError`, `HashMismatchError`.
- **Testing subpackage** — `dryade_plugins_sdk.testing` ships `FakeHost`,
`FakeRegistry`, `MockKV`, `MockConfig`, `MockLLM`, `LLMCall` and the
`build_plugin` / `build_agent` / `build_tool` factories so authors can
pytest their plugins without installing Dryade core (D-07).
pytest their plugins without installing the host runtime.
- **Decorators** — `@tool(name=..., description=...)`,
`@route(path=..., method=...)`, `@plugin_metadata(...)` for ergonomic
scaffolding.
Expand All @@ -70,8 +70,8 @@ plugin authors install from PyPI.
- **Examples** — five reference plugins (`hello_world`, `with_tool`,
`with_llm`, `with_ui`, `multi_agent`) under `examples/`.
- **CI workflows** — full matrix on Python 3.11 / 3.12 / 3.13 + defense-in-depth
(D-05 zero-core-imports gate, Rule §9 hash conformance gate,
Rule §11 community-tier rejection gate), hermetic E2E smoke,
(zero host-runtime imports gate, hash-conformance gate,
community-tier rejection gate), hermetic E2E smoke,
OIDC trusted publishing to PyPI, OpenSSF Scorecard, CodeQL Python
analysis, dependency-review on PRs, dependency-review-action.
- **Community files** — `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`,
Expand All @@ -83,15 +83,15 @@ plugin authors install from PyPI.

- Plugins ship with author Ed25519 dev keys generated by
`dryade plugin keygen`. Key permissions are enforced to `0o600` at
generation time (D-08).
generation time.
- `.dryadepkg` is a gzipped tar archive containing a v2 manifest with
`plugin_hash_sha256` + `plugin_hash_sha3_256` + 128-char hex Ed25519
signature. The marketplace verifies + re-signs into the production
allowlist; authors never see signing material from the platform.
- The SDK and CLI have **zero** `core.*` imports — verified by
`tests/test_zero_core_imports.py` running in CI on every PR.
- `--tier community` is rejected at validate, scaffold, and package time
(Rule §11).
(`community` is not a valid plugin tier).

### Notes

Expand Down
16 changes: 8 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,33 @@ Thanks for your interest in contributing.
Your PR must pass:

- `pytest` (full suite — Python 3.11, 3.12, 3.13)
- `pytest tests/test_zero_core_imports.py` (D-05 enforcement — SDK NEVER imports from the host runtime)
- `pytest tests/test_hash_conformance.py` (Rule §9 — hash algorithm byte-identical to Dryade core)
- `pytest tests/test_zero_core_imports.py` (enforcement — the SDK NEVER imports from the host runtime)
- `pytest tests/test_hash_conformance.py` (hash algorithm byte-identical to the host runtime)
- `pytest tests/test_smoke_e2e.py` (scaffold → validate → package smoke)
- `ruff check src/ tests/` (lint)
- `mypy src/dryade_plugins_sdk` (typecheck)

## Non-negotiable invariants

1. **D-05: zero host-runtime imports.** This SDK is a pure CONTRACT package. It defines
Protocols that the Dryade runtime implements, not the other way around. the host-imports gate test
1. **Zero host-runtime imports.** This SDK is a pure CONTRACT package. It defines
Protocols that the Dryade runtime implements, not the other way around. The host-imports gate test
AST-scans every file in `src/` and fails the build on any `from core.` or `import core`.

2. **Rule §9: Hash algorithm parity.** `compute_plugin_hash_pair` must produce
2. **Hash algorithm parity.** `compute_plugin_hash_pair` must produce
byte-identical SHA-256 and SHA3-256 digests as the Dryade runtime's hash function. The `test_hash_conformance.py` test
independently reimplements the algorithm and asserts equality.

3. **Rule §11: Tier names.** Valid `required_tier` values: `starter`, `team`,
3. **Tier names.** Valid `required_tier` values: `starter`, `team`,
`enterprise`. **Never** add `community`, `dev`, `sovereign`, or any other.
The schema enum is locked.

4. **Fail-closed everywhere.** No `--skip-X`, no `--unsafe-Y`, no
`DRYADE_DISABLE_*` env-vars in the SDK or CLI.

5. **Public docs only.** The SDK is the public face of Dryade plugin authoring.
Documentation must not leak internal mechanics: TOFU pubkey paths, allowlist
Documentation must not leak internal mechanics: pinned-pubkey paths, allowlist
format, marketplace internals, port numbers. See
`tests/test_security_disclosure.py` for the forbidden-pattern list.
`tests/test_no_internal_leaks.py` for the forbidden-pattern list.

## Releasing

Expand Down
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ https://dryade.ai/security.

In-scope examples (non-exhaustive):

- A bug in `dryade-cli` that causes the packager to emit a `.dryadepkg`
whose embedded hash does not match the bundled source (`Rule §9` drift).
- A bug in the CLI that causes the packager to emit a `.dryadepkg`
whose embedded hash does not match the bundled source (hash drift).
- A flaw in the SDK that lets an author bypass `--tier community`
rejection at validate time (`Rule §11`).
rejection at validate time.
- Any path in the SDK or CLI that exfiltrates the author's private signing
key off disk.

Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ fields. Mutates and returns the manifest.
### `load_private_key(path: Path)`

Loads an Ed25519 private key, asserting mode `0o600` (fails closed
otherwise — D-08).
otherwise).

### `verify_plugin_hash(manifest: dict, plugin_dir: Path) -> bool`

Expand All @@ -135,7 +135,7 @@ loader.

## Testing fixtures (`dryade_plugins_sdk.testing`)

D-07 fixtures so authors `pytest` without installing Dryade:
Fixtures so authors `pytest` without installing the host runtime:

- `FakeHost` — in-memory host. Use `host.load(plugin)` then assert against
`host.registry.tools`, `host.registry.routes`, `host.registry.agents`,
Expand Down
14 changes: 7 additions & 7 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Output:
~/.dryade-author/dev-key.pub
```

The CLI refuses to load keys with weaker permissions (D-08). Re-run keygen
The CLI refuses to load keys with weaker permissions. Re-run keygen
to rotate; the marketplace records each public key fingerprint you submit
under, so rotated keys remain valid for previously-signed packages.

Expand All @@ -40,7 +40,7 @@ dryade plugin new my_plugin --tier starter

**Options:**

- `--tier {starter,team,enterprise}` — Rule §11 enforces this set.
- `--tier {starter,team,enterprise}` — the CLI enforces this set.
- `--out PATH` — output directory (default: current working dir).
- `--description STRING` — overrides default placeholder.
- `--author STRING` — author field (default: empty placeholder).
Expand All @@ -64,10 +64,10 @@ dryade plugin validate ./my_plugin
Checks (in order):

1. Manifest schema (Draft 2020-12).
2. `--tier community` rejection (Rule §11).
2. `--tier community` rejection (`community` is not a valid plugin tier).
3. `__init__.py` + `pyproject.toml` presence.
4. Plugin Protocol conformance (`isinstance(plugin, Plugin)`).
5. Custom-plugin-slot disclosure if relevant (D-10).
5. Custom-plugin-slot disclosure if relevant.

Exit `0` on success, `1` on first failure (each error is named in stdout).

Expand All @@ -83,7 +83,7 @@ dryade plugin package ./my_plugin --output ./dist
**Options:**

- `--output PATH` — output directory (default: `./dist/`).
- `--no-verify` does not exist. Fail-closed (CLAUDE.md Rule §4).
- `--no-verify` does not exist. Fail-closed.

Output: `<name>-<version>.dryadepkg` — a gzipped tar archive containing:

Expand All @@ -95,7 +95,7 @@ Output: `<name>-<version>.dryadepkg` — a gzipped tar archive containing:
source without unpacking the SBOM.
- The plugin's `__init__.py`, `plugin.py`, and any other `.py` files.

Author key material is **never** bundled (T-339-04-03 mitigation).
Author key material is **never** bundled.

## `dryade plugin doctor`

Expand All @@ -121,4 +121,4 @@ The CLI honors:
- `HOME` — author keys land under `$HOME/.dryade-author/`.

It does **not** honor any `DRYADE_DISABLE_*` or `--unsafe-*` toggles —
fail-closed everywhere (Rule §4 of CLAUDE.md).
fail-closed everywhere.
10 changes: 5 additions & 5 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ version) has a strict JSON schema with these required fields:
| `name` | string, snake_case | matches the directory name |
| `version` | semver | strict 2.0.0 |
| `description` | string | shown in marketplace cards |
| `required_tier` | `starter` / `team` / `enterprise` | **Rule §11 — never `community`** |
| `required_tier` | `starter` / `team` / `enterprise` | **never `community`** |
| `author` | string | your name or org |
| `core_version_constraint` | PEP 440 spec | e.g. `>=1.0.0,<2.0.0` |

Expand All @@ -59,8 +59,8 @@ the full surface.

Tier limits are **enforced by the Dryade host** based on the signed
allowlist, not by the SDK or CLI. The CLI rejects `--tier community` at
scaffold + validate + package time (Rule §11) because the platform itself
has no community tier.
scaffold + validate + package time because `community` is not a valid
plugin tier.

## Signing

Expand All @@ -70,8 +70,8 @@ with the private key. Private keys live at `~/.dryade-author/dev-key.priv`
with mode `0o600` — the CLI refuses to load weaker permissions.

When you submit to the marketplace, the marketplace **re-signs** with its
own dual Ed25519 + ML-DSA-65 keys before publishing in the signed allowlist.
Authors never see the production signing material.
own keys (including a post-quantum signature) before publishing in the signed
allowlist. Authors never see the production signing material.

## SBOM — embedded CycloneDX

Expand Down
3 changes: 1 addition & 2 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ dryade plugin package

The repo's `tests/test_examples_build.py` builds every example on every
PR — manifest validation, plugin import, file shape check, and a
full pytest-subprocess run. T-09-3 mitigation: examples cannot silently
break.
full pytest-subprocess run, so examples cannot silently break.

## Submitting a new example

Expand Down
14 changes: 7 additions & 7 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ Three reasons:
It tells the host's allowlist what privilege class your plugin needs.
Valid values: `starter`, `team`, `enterprise`. The host enforces tier
limits (max_users, custom_plugin_slots) from the signed allowlist;
the SDK refuses `community` (Rule §11) at every level.
the SDK refuses `community` at every level.

## 4. Can I bypass the hash check for development?

**No.** The CLI is fail-closed (Rule §4). The host accepts plugins whose
**No.** The CLI is fail-closed. The host accepts plugins whose
on-disk hash matches the manifest-embedded hash, full stop. For local
iteration, re-run `dryade plugin package` after every code change — it
takes <1s and produces a fresh hash.
Expand Down Expand Up @@ -69,17 +69,17 @@ Protocols, not the other way around.

## 8. Why is `community` rejected as a tier?

The Dryade platform has no community tier — community users have no
Plugin Manager and no plugins at all. Plugins ship to paying customers
(`starter` / `team` / `enterprise`). Rule §11 enforces this at every
The Dryade platform has no community tier. Plugins ship to paying customers
(`starter` / `team` / `enterprise`), and the SDK enforces this at every
scaffold / validate / package step.

## 9. How do I get my plugin into the marketplace?

Submit the `.dryadepkg` produced by `dryade plugin package` to the
marketplace at https://dryade.ai/marketplace. The marketplace verifies
your dev signature, re-signs with its dual Ed25519 + ML-DSA-65 production
keys, and publishes in the signed allowlist that customer installs pull.
your dev signature, re-signs with its own production keys (including a
post-quantum signature), and publishes in the signed allowlist that customer
installs pull.

## 10. Can I distribute plugins outside the marketplace?

Expand Down
10 changes: 5 additions & 5 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ and covers vulnerability reporting.
- `~/.dryade-author/dev-key.priv` — mode `0o600`, fail-closed at load.
- `~/.dryade-author/dev-key.pub` — embedded into the `.dryadepkg`.

The CLI refuses to load keys with weaker permissions (D-08). Rotate by
The CLI refuses to load keys with weaker permissions. Rotate by
re-running keygen; the marketplace tracks public-key fingerprints, so
old signatures remain valid for already-submitted packages.

**Never check author keys into source control.** The `.gitignore` shipped
by the scaffolder excludes `~/.dryade-author/` already; if you create
custom paths, ignore those too.

## Hash conformance (Rule §9)
## Hash conformance

Every plugin source file (every `.py` under your plugin directory, excluding
`__pycache__/`) gets dual-hashed:
Expand All @@ -37,7 +37,7 @@ on-disk source drifts from the manifest-embedded hash. Authors who
modify code after packaging must re-run `dryade plugin package` to
produce a fresh signed archive.

## Tier slots (Rule §11)
## Tier slots

`required_tier` must be `starter`, `team`, or `enterprise`. **Never
`community`.** The CLI rejects `community` at scaffold + validate +
Expand All @@ -59,8 +59,8 @@ customer's tier must be ≥ this value.
The plugin runs after the host has already loaded the allowlist and
verified your signature. From inside a plugin you cannot:

- Read the customer's `~/.dryade/allowed-plugins.json`.
- Read the PM's TOFU-pinned signing key.
- Read the customer's allowlist.
- Read the platform's pinned signing key.
- Force a re-signing pass.

Your Leash sandbox + the Plugin Tool Bridge enforce these boundaries.
Expand Down
Loading
Loading