feat(plugins): add managed runtime SDK and release 2.4.5

This commit is contained in:
chenmingxuan committed 2026-10-30 14:39:21 +08:00
1 parent 506e3bf1c5
commit 2a3bbaa3fb
45 files changed
+3348 -98

No files matched your search

+326
View File
@@ -0,0 +1,326 @@
"""Market validation uses synthetic packages/evidence, never a real release."""
import hashlib
import io
import json
from pathlib import Path
import sys
import zipfile
import pytest
from artemis.plugins.market import (
CHECKS,
REPORT_NAME,
content_fingerprint,
draft_submission,
package_files,
package_directory,
validate_submission,
)
from artemis.plugins.models import PluginManifest
from artemis.plugins.validation import check, main
from artemis.plugins import validation
@pytest.fixture
def candidate(tmp_path):
manifest = PluginManifest.model_validate(
{
"schema_version": 2,
"id": "synthetic-market-test",
"name": "Synthetic",
"version": "1.0.0",
"platforms": [sys.platform],
"skill": "SKILL.md",
"license": "Apache-2.0",
"capabilities": {"kind": "file", "tools": {}},
}
)
(tmp_path / "plugin.json").write_text(manifest.model_dump_json(), encoding="utf-8")
for name in ("SKILL.md", "LICENSE", "SOURCE.md", "VERIFICATION.md"):
(tmp_path / name).write_text(
"Synthetic test fixture, not a published claim.\n", encoding="utf-8"
)
return tmp_path, manifest
def submitted(candidate):
root, manifest = candidate
files = package_files(root)
report = draft_submission(manifest, files)
report["traits"] = {"uses_subprocesses": False, "file_inputs_for_writes": False}
report["components"] = [
{
"name": "synthetic fixture",
"paths": ["."],
"license": "Apache-2.0",
"permission": "permitted",
"license_file": "LICENSE",
"source_file": "SOURCE.md",
}
]
for record in report["checks"].values():
record.update(
status="passed",
evidence_file="VERIFICATION.md",
evidence_sha256=files["VERIFICATION.md"],
notes="Synthetic validation record for unit testing only.",
)
write_report(root, report)
return root, manifest, report
def write_report(root, report):
(root / REPORT_NAME).write_text(json.dumps(report), encoding="utf-8")
def validate(root, manifest):
return validate_submission(root, manifest, package_files(root))
@pytest.mark.asyncio
async def test_local_development_does_not_require_public_distribution(candidate):
root, _ = candidate
result = await check(root, False, {})
assert result["scope"] == "development"
assert not (root / REPORT_NAME).exists()
with pytest.raises(ValueError, match="MARKET.json"):
await check(root, False, {}, market=True)
@pytest.mark.asyncio
async def test_generated_market_draft_is_unverified_and_never_overwrites(candidate):
root, _ = candidate
result = await check(root, False, {}, prepare_market=True)
assert result["status"] == "draft"
report = json.loads((root / REPORT_NAME).read_text("utf-8"))
assert all(record["status"] == "unverified" for record in report["checks"].values())
assert report["components"][0]["permission"] == "unknown"
assert report["traits"]["uses_subprocesses"] is None
before = (root / REPORT_NAME).read_bytes()
with pytest.raises(FileExistsError):
await check(root, False, {}, prepare_market=True)
assert (root / REPORT_NAME).read_bytes() == before
with pytest.raises(ValueError):
await check(root, False, {}, market=True)
@pytest.mark.asyncio
async def test_valid_market_materials_report_and_zip_have_same_content_hash(
candidate, tmp_path_factory
):
root, manifest, report = submitted(candidate)
checked = await check(root, False, {}, market=True)
assert checked["scope"] == "market_materials"
archive = tmp_path_factory.mktemp("market-zip") / "candidate.zip"
with zipfile.ZipFile(archive, "w") as output:
for path in root.iterdir():
output.write(path, path.name)
zipped = await check(archive, False, {}, market=True)
assert zipped["content_sha256"] == checked["content_sha256"] == report["content_sha256"]
assert zipped["archive_sha256"] == hashlib.sha256(archive.read_bytes()).hexdigest()
@pytest.mark.parametrize("permission", ["unknown", "local_only"])
def test_market_rejects_components_without_public_distribution(candidate, permission):
root, manifest, report = submitted(candidate)
report["components"][0]["permission"] = permission
write_report(root, report)
with pytest.raises(ValueError, match="市场分发"):
validate(root, manifest)
def test_market_rejects_changed_payload_and_forged_evidence(candidate):
root, manifest, report = submitted(candidate)
report["checks"]["source_review"]["evidence_sha256"] = "0" * 64
write_report(root, report)
with pytest.raises(ValueError, match="记录 SHA256"):
validate(root, manifest)
(root / "SKILL.md").write_text("different bytes", encoding="utf-8")
with pytest.raises(ValueError, match="旧的市场验证记录失效"):
validate(root, manifest)
@pytest.mark.parametrize(
"change",
[
"version",
"missing_check",
"unverified",
"no_notes",
"uncovered",
"source_escape",
"license_missing",
"na_required",
"process_na",
"binding_na",
],
)
def test_market_blocks_incomplete_or_inapplicable_claims(candidate, change):
root, manifest, report = submitted(candidate)
if change == "version":
report["plugin_version"] = "2.0.0"
elif change == "missing_check":
del report["checks"]["real_task"]
elif change == "unverified":
report["checks"]["real_task"]["status"] = "unverified"
elif change == "no_notes":
report["checks"]["source_review"]["notes"] = " "
elif change == "uncovered":
report["components"][0]["paths"] = ["LICENSE"]
elif change == "source_escape":
report["components"][0]["source_file"] = "../outside"
elif change == "license_missing":
report["components"][0]["license_file"] = "missing.txt"
elif change == "na_required":
report["checks"]["real_task"]["status"] = "not_applicable"
elif change == "process_na":
report["traits"]["uses_subprocesses"] = True
report["checks"]["process_tree"]["status"] = "not_applicable"
elif change == "binding_na":
report["traits"]["file_inputs_for_writes"] = True
report["checks"]["file_binding"]["status"] = "not_applicable"
write_report(root, report)
with pytest.raises(ValueError):
validate(root, manifest)
@pytest.mark.parametrize(
"name", [".ruff_cache/entry", "__pycache__/x", "module.pyc", ".env", ".env.local"]
)
def test_market_blocks_cache_and_personal_configuration(candidate, name):
root, _ = candidate
path = root / name
path.parent.mkdir(exist_ok=True)
path.write_text("synthetic only", encoding="utf-8")
with pytest.raises(ValueError, match="市场包"):
package_files(root)
def test_fingerprint_is_order_independent_but_binds_paths_and_bytes():
assert content_fingerprint({"b": "2", "a": "1"}) == content_fingerprint({"a": "1", "b": "2"})
assert content_fingerprint({"a": "1"}) != content_fingerprint({"a": "2"})
assert content_fingerprint({"a": "1"}) != content_fingerprint({"b": "1"})
@pytest.mark.asyncio
async def test_zip_path_traversal_cannot_escape_inspection(candidate, tmp_path_factory):
root, _ = candidate
archive = tmp_path_factory.mktemp("bad-market-zip") / "bad.zip"
with zipfile.ZipFile(archive, "w") as output:
output.writestr("../outside.txt", "synthetic")
with pytest.raises(ValueError):
await check(archive, False, {})
assert not (archive.parent / "outside.txt").exists()
def test_json_output_must_not_modify_candidate_or_overwrite_zip(candidate, monkeypatch, capsys):
root, _ = candidate
target = root / "report.json"
monkeypatch.setattr(sys, "argv", ["check_plugin", str(root), "--json-output", str(target)])
with pytest.raises(SystemExit):
main()
assert not target.exists()
archive = root / "original.zip"
archive.write_bytes(b"original immutable zip placeholder")
monkeypatch.setattr(sys, "argv", ["check_plugin", str(archive), "--json-output", str(archive)])
with pytest.raises(SystemExit):
main()
assert archive.read_bytes() == b"original immutable zip placeholder"
def test_invalid_values_are_not_echoed(candidate, monkeypatch, capsys):
root, _ = candidate
private_marker = "SYNTHETIC-DO-NOT-ECHO"
monkeypatch.setattr(
sys, "argv", ["check_plugin", str(root), "--values", '{"key":"' + private_marker]
)
with pytest.raises(SystemExit):
main()
assert private_marker not in capsys.readouterr().out
def add_synthetic_server(candidate):
root, manifest = candidate
data = manifest.model_dump(mode="json")
data["server"] = {"command": sys.executable, "args": ["synthetic-unused-server.py"]}
manifest = PluginManifest.model_validate(data)
(root / "plugin.json").write_text(manifest.model_dump_json(), encoding="utf-8")
return root, manifest
def handshake_fixture(monkeypatch, action):
class FakeSession:
tools = []
def __init__(self, server, root):
self.root = root
async def start(self):
action(self.root)
return self
async def aclose(self):
pass
monkeypatch.setattr(validation, "PluginSession", FakeSession)
@pytest.mark.asyncio
@pytest.mark.parametrize("changed", ["payload", "submission"])
async def test_market_rejects_changes_during_handshake(candidate, monkeypatch, changed):
root, manifest, report = submitted(add_synthetic_server(candidate))
def change(directory):
if changed == "payload":
(directory / "SKILL.md").write_text("changed during handshake", encoding="utf-8")
else:
report["checks"]["real_task"]["notes"] = "A different synthetic claim."
write_report(directory, report)
handshake_fixture(monkeypatch, change)
with pytest.raises(ValueError, match="发生变化"):
await check(root, False, {}, market=True)
@pytest.mark.asyncio
async def test_zip_digest_is_bound_to_bytes_actually_inspected(
candidate, tmp_path_factory, monkeypatch
):
root, _, report = submitted(add_synthetic_server(candidate))
archive = tmp_path_factory.mktemp("changing-zip") / "candidate.zip"
with zipfile.ZipFile(archive, "w") as output:
for path in root.iterdir():
output.write(path, path.name)
approved = hashlib.sha256(archive.read_bytes()).hexdigest()
handshake_fixture(monkeypatch, lambda root: archive.write_bytes(b"uninspected replacement"))
with pytest.raises(ValueError, match="ZIP 发生变化"):
await check(archive, False, {}, market=True)
assert approved != hashlib.sha256(archive.read_bytes()).hexdigest()
def test_archive_fingerprint_uses_extracted_snapshot(candidate, tmp_path_factory):
root, _ = candidate
archive = tmp_path_factory.mktemp("zip-snapshot") / "candidate.zip"
with zipfile.ZipFile(archive, "w") as output:
for path in root.iterdir():
output.write(path, path.name)
approved = hashlib.sha256(archive.read_bytes()).hexdigest()
with pytest.raises(ValueError, match="ZIP 发生变化"):
with package_directory(archive) as (extracted, digest):
archive.write_bytes(b"uninspected replacement")
assert (extracted / "plugin.json").read_bytes() == (root / "plugin.json").read_bytes()
assert digest == approved
@pytest.mark.asyncio
async def test_incomplete_materials_never_launch_server(candidate, monkeypatch):
root, _ = add_synthetic_server(candidate)
def never_start(root):
raise AssertionError("Incomplete market materials launched a plugin")
handshake_fixture(monkeypatch, never_start)
with pytest.raises(ValueError, match="MARKET.json"):
await check(root, False, {}, market=True)