Task — engineering-spec@1

"Plugin manifest schema v1.0.0 — canonical plugin.json validated against manifest.schema.json with cyberos-plugin pack reference packer"

draftTASK-PLUGIN-001
module plugin · class product · priority p0 · created 2026-05-19 · shipped null
depends on none · blocks TASK-PLUGIN-002, TASK-PLUGIN-003, TASK-PLUGIN-004, TASK-PLUGIN-005, TASK-PLUGIN-006, TASK-PLUGIN-007, TASK-PLUGIN-008

§1 — Description (BCP-14 normative)

The PLUGIN module MUST ship the manifest schema and reference packer at modules/plugin/cyberos_plugin/. The schema is JSONSchema 2020-12 at modules/plugin/manifest.schema.json; the packer is Python at modules/plugin/cyberos_plugin/packer.py with CLI entrypoint cyberos-plugin pack. Together they define how a CyberOS plugin bundle is described, validated, and produced.

  1. MUST validate canonical manifests against manifest.schema.json per DEC-2400. Validation runs at three sites: (a) author time via cyberos-plugin validate manifests/<id>@<version>.plugin.json; (b) pack time via cyberos-plugin pack (refuses to emit a bundle if the manifest doesn't validate); (c) install time via the host runtime (each adapter inherits the schema). Validation errors MUST be human-readable with the failing JSON-pointer and the constraint that failed.
  1. MUST declare schema_version as the const "1.0.0" per DEC-2401. Schema evolutions are handled by writing task-PLUGIN-001a (then b, then c) — never by widening the v1 schema in place. This keeps every shipped plugin pinned to a known schema generation.
  1. MUST enforce the id pattern ^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$ per DEC-2402 — lowercase kebab-case, 3-64 characters, no leading/trailing hyphen. This pattern is OCI-image-name compatible so marketplace artefacts can be addressed as oci://plugins.cyberskill.world/<id>:<version> without transformation.
  1. MUST enforce SemVer 2.0 on the version field per DEC-2403. The validator accepts MAJOR.MINOR.PATCH, MAJOR.MINOR.PATCH-PRERELEASE (e.g. 1.0.0-beta.1), and MAJOR.MINOR.PATCH+BUILD (e.g. 1.0.0+sha.abc123). Build metadata MUST NOT affect compatibility — two manifests with version: "1.0.0+a" and version: "1.0.0+b" are semantically equal.
  1. MUST restrict capabilities to the closed enum of 7 keys per DEC-2404: read_memory, write_memory, execute_workflow, list_skills, invoke_skill, publish_skill, route_natural_language. additionalProperties is false. Adding a capability requires bumping the schema version per DEC-2401.
  1. MUST enforce the SEP-986 tool name pattern ^cyberos\.[a-z][a-z0-9]*\.[a-z][a-z0-9_]*$ per DEC-2405. Each tool's name MUST start with cyberos., then a module slug, then a verb-underscore-noun pair (e.g. cyberos.cuo.execute_workflow, cyberos.memory.read_audit, cyberos.skill.list_catalog).
  1. MUST enforce auth.method == "oauth-pkce" as a JSONSchema const per DEC-2406. Other auth methods (api-key, basic-auth, bearer-static) MUST be rejected at validation. Future versions MAY add methods via task-PLUGIN-001a but v1 is locked.
  1. MUST require signature.rekor_uuid and signature.sigstore_bundle per DEC-2407 — these fields are at the top of manifest.schema.json#/required. A manifest cannot validate without a Sigstore Rekor transparency-log entry referenced. The packer (clause 11) refuses to emit unsigned bundles.
  1. MUST validate capability/scope coherence: a manifest declaring capabilities.write_memory: true MUST also list at least one tool whose scopes array contains cyberos:memory:write. This is a JSONSchema-extension check in validator.py, not pure schema. Reason: hosts surface declared capabilities at install time; if the declaration doesn't match the actual scopes requested, users grant under false pretences.
  1. MUST produce reproducible bundles per DEC-2409. packer.py MUST: (a) set every zip entry's mtime to a fixed epoch (1980-01-01 00:00:00 UTC — the zip-format minimum); (b) sort entries by path; (c) use fixed permissions (0o644 for files, 0o755 for directories); (d) include no machine names, user IDs, or local paths in any file. Two builds of the same manifest from the same source MUST produce identical SHA-256.
  1. MUST expose CLI cyberos-plugin pack <manifest> [--out <path>] and cyberos-plugin validate <manifest> and cyberos-plugin doctor <bundle>. pack produces a zip containing plugin.json + commands/ + skills/ + adapter assets. validate is dry-run schema check. doctor opens an existing bundle and checks the 8 INTEROP invariants.
  1. MUST NOT allow additionalProperties at the top level of the manifest per DEC-2400. Unknown fields are rejected to keep the contract closed. Adapter-specific fields go under targets[].extensions{} (introduced in TASK-PLUGIN-007).
  1. MUST NOT silently coerce types (e.g. version: 1.0"1.0.0"). Type mismatches fail validation with a clear error.
  1. MUST NOT accept manifests where any tool's annotations.destructive == true but scopes does not include a corresponding write scope. Per TASK-MCP-006 tool gating, destructive tools require explicit scope coverage.

§2 — Why this design

Why JSONSchema 2020-12 (DEC-2400)? Latest stable JSONSchema draft; broad tooling support (Python jsonschema 4.x, Rust jsonschema, JS ajv 8.x); supports const, additionalProperties: false, pattern, format, enum — every constraint we need. Older drafts (draft-07) lack some constraints we use; draft-2019-09 is superseded.

Why const schema_version (DEC-2401)? Allowing a range (e.g. ^1\\..*$) invites silent compatibility drift — fields appear in v1.3 manifests that v1.0 validators don't recognise. Constant versions force every new field to ship under a new task with explicit ecosystem migration guidance. This is the Lockfile pattern, applied to plugin schemas.

Why OCI-compatible id pattern (DEC-2402)? Strategy §4 Level 3 envisions a marketplace addressable as oci://plugins.cyberskill.world/<id>:<version>. If id accepts characters OCI rejects (uppercase, underscores, dots), the marketplace adapter has to transform before push, breaking round-trip identity. Keeping the patterns aligned avoids the transformation entirely.

Why SemVer 2.0 (DEC-2403)? Plugin consumers (hosts) need a well-known compatibility model. SemVer is the industry default. Pre-release identifiers (1.0.0-beta.1) are essential for staged rollouts. Build metadata is semantically irrelevant per SemVer spec — two builds of the same source can carry different +sha.x and still be the same plugin.

Why closed capabilities enum (DEC-2404)? Hosts render the capability list at install time so the user knows what they're granting. If capabilities are an open string set, malicious plugins could declare misleading capability names (harmless_memory_lookup instead of read_memory) that users skim past. A closed enum forces every capability to be reviewed and named in this task.

Why SEP-986 enforced at manifest level (DEC-2405, clause 6)? TASK-MCP-003 already enforces this at the gateway. Enforcing it at the manifest schema level catches violations earlier (author time, not runtime) and surfaces them in the same validator output as other schema errors. Defense-in-depth.

Why OAuth-PKCE only in v1 (DEC-2406, clause 7)? Long-lived secrets in bundles are a supply-chain liability — anyone who exfiltrates a bundle gets the key. OAuth-PKCE forces a per-install handshake against auth.cyberskill.world, with token rotation. The threat model is "stolen bundle MUST NOT yield credentials." API keys violate that; OAuth-PKCE preserves it.

Why required Rekor UUID (DEC-2407, clause 8)? Strategy §2 lists "open audit chain" as one of CyberOS's four defensible positions. A plugin without a Rekor anchor has no audit chain — anyone could swap the bytes after publication. Making the field required at the schema level means the absence is caught immediately, not at publish time.

Why Python reference packer (DEC-2408, clause 11)? Matches the CUO + memory convention (Python reference impl + Rust production binary). Python is faster to iterate, easier to fuzz, and jsonschema is mature. The Rust production binary (services/plugin-host/) lands in TASK-PLUGIN-007 alongside the multi-runtime adapters.

Why reproducibility (DEC-2409, clause 10)? Sigstore Rekor proves "this artefact was signed by X at time Y." That proof is only useful if a verifier can rebuild the bundle locally and confirm the hash matches. Non-reproducible bundles break that round-trip — you can verify the signature, but you can't verify the bytes came from the source. Reproducibility makes Sigstore actually work.

Why JSON-pointer error messages (clause 1)? Plugin authors will hit validation errors. path /tools/3/name: pattern '^cyberos\\..*$' violated is actionable. validation failed is not.

Why capability/scope coherence check (clause 9)? A common bug pattern: developer adds a capability declaration but forgets to add the corresponding scope in tools[*].scopes. Host surfaces the wrong consent UI. The cross-field check catches this at validate time.


§3 — API contract

manifest.schema.json (already at modules/plugin/manifest.schema.json)

The full schema is in the file. Required top-level keys: schema_version, id, version, name, description, authors, license, capabilities, tools, auth, audit, targets, signature. See the file for the full property definitions.

Python packer surface (cyberos_plugin/packer.py)

from dataclasses import dataclass
from pathlib import Path
from typing import Sequence

@dataclass(frozen=True)
class PackResult:
    bundle_path: Path
    sha256: str            # reproducible hash
    size_bytes: int
    tools_count: int
    commands_count: int
    skills_count: int

def pack(
    manifest_path: Path,
    target: str = "claude-code",      # one of {claude-code,cursor,cowork,codex-cli}
    out_dir: Path = Path("dist"),
    *,
    fail_on_unsigned: bool = True,    # DEC-2407
    reproducible: bool = True,        # DEC-2409
) -> PackResult: ...

def validate(manifest_path: Path) -> Sequence[ValidationError]: ...

def doctor(bundle_path: Path) -> Sequence[InvariantViolation]: ...

CLI surface (cyberos_plugin/cli.py)

$ cyberos-plugin pack <manifest> [--target {claude-code,cursor,cowork,codex-cli}] [--out <dir>]
$ cyberos-plugin validate <manifest>
$ cyberos-plugin doctor <bundle.plugin>
$ cyberos-plugin --version

Minimal valid manifest

{
  "schema_version": "1.0.0",
  "id": "cyberos",
  "version": "1.0.0",
  "name": "CyberOS",
  "description": "Persona-aware orchestration + memory + skills for any agentic IDE.",
  "authors": [{"name": "CyberSkill Software", "url": "https://cyberskill.world"}],
  "license": "Apache-2.0",
  "capabilities": {"read_memory": true, "execute_workflow": true, "list_skills": true},
  "tools": [
    {
      "name": "cyberos.cuo.list_personas",
      "description": "List the 47 active CyberOS personas available for orchestration.",
      "input_schema": {"type": "object", "properties": {}},
      "scopes": ["cyberos:cuo:list"]
    }
  ],
  "auth": {
    "method": "oauth-pkce",
    "authorize_url": "https://auth.cyberskill.world/v1/oauth/authorize",
    "token_url": "https://auth.cyberskill.world/v1/oauth/token",
    "scopes": ["cyberos:cuo:list"]
  },
  "audit": {
    "endpoint": "https://memory.cyberskill.world/v1/audit",
    "kinds": ["plugin.installed", "plugin.invoked", "plugin.uninstalled", "plugin.updated"]
  },
  "targets": ["claude-code"],
  "signature": {
    "sigstore_bundle": "<base64-encoded-sigstore-bundle>",
    "rekor_uuid": "24296fb24b8ad77a..."
  }
}

§4 — Acceptance criteria

  1. Schema rejects missing schema_version — fixture invalid_missing_schema_version.json fails validation with path /: required property 'schema_version' missing.
  2. Schema rejects schema_version != "1.0.0" — fixture with schema_version: "1.1.0" fails with const violation.
  3. Schema rejects uppercase id — fixture with id: "CyberOS" fails the pattern.
  4. Schema rejects id < 3 chars — fixture with id: "ab" fails.
  5. Schema rejects malformed version — fixture with version: "1.0" fails the SemVer pattern.
  6. Schema accepts SemVer pre-releaseversion: "1.0.0-beta.1" passes.
  7. Schema accepts SemVer build metadataversion: "1.0.0+sha.abc123" passes.
  8. Schema rejects unknown capability — fixture with capabilities.hack_the_planet: true fails additionalProperties.
  9. Schema rejects non-SEP-986 tool nametools[0].name: "foo.bar" fails pattern.
  10. Schema rejects auth.method != "oauth-pkce"auth.method: "api-key" fails const.
  11. Schema rejects missing signature.rekor_uuid — fixture invalid_missing_signature.json fails.
  12. Validator catches capability/scope mismatch — manifest declaring write_memory: true but no tool with cyberos:memory:write scope fails with a custom error message.
  13. Validator catches destructive-without-write-scope — tool with annotations.destructive: true and no write scope fails.
  14. Packer refuses unsigned bundle when fail_on_unsigned=True — call packer with manifest missing signature → raises UnsignedManifestError.
  15. Packer produces reproducible SHA-256 — two pack() calls on the same manifest from the same source produce identical SHA-256.
  16. Packer entries have epoch mtimeunzip -l bundle.plugin shows all entries timestamped 1980-01-01.
  17. Packer entries are sorted by pathunzip -l shows ascending path order.
  18. Packer file permissions are 0o644unzip -l shows -rw-r--r-- on every file.
  19. Packer rejects unknown targetpack(..., target="bogus") raises UnknownTargetError.
  20. CLI pack returns exit 0 on successcyberos-plugin pack manifests/cyberos@1.0.0.plugin.json writes a file and exits 0.
  21. CLI validate returns exit 1 on failurecyberos-plugin validate invalid_missing_signature.json prints error to stderr, exits 1.
  22. CLI doctor catches missing Rekor on existing bundle — running doctor on a bundle whose plugin.json has signature stripped fails.
  23. Validation errors include JSON-pointer paths — error messages contain /tools/0/name style references, not "line 47."

§5 — Verification

# tests/test_schema_required_fields.py
import json, jsonschema, pathlib

SCHEMA = json.loads(pathlib.Path("modules/plugin/manifest.schema.json").read_text())

def test_minimal_valid_passes():
    m = json.loads(pathlib.Path("tests/fixtures/valid_minimal_plugin.json").read_text())
    jsonschema.validate(m, SCHEMA)  # no exception

def test_missing_schema_version_fails():
    m = json.loads(pathlib.Path("tests/fixtures/valid_minimal_plugin.json").read_text())
    del m["schema_version"]
    with pytest.raises(jsonschema.ValidationError) as e:
        jsonschema.validate(m, SCHEMA)
    assert "schema_version" in str(e.value)

def test_schema_version_const():
    m = _load_minimal()
    m["schema_version"] = "1.1.0"
    with pytest.raises(jsonschema.ValidationError):
        jsonschema.validate(m, SCHEMA)
# tests/test_schema_tool_name_pattern.py
@pytest.mark.parametrize("name,valid", [
    ("cyberos.cuo.execute_workflow", True),
    ("cyberos.memory.read_audit", True),
    ("cyberos.skill.list_catalog", True),
    ("CyberOS.cuo.execute", False),         # uppercase
    ("cyberos.execute", False),             # missing module segment
    ("foo.bar.baz", False),                 # wrong prefix
    ("cyberos..execute_workflow", False),   # empty module
    ("cyberos.cuo.123_execute", False),     # verb starts with digit
])
def test_tool_name_pattern(name, valid):
    m = _load_minimal()
    m["tools"][0]["name"] = name
    if valid:
        jsonschema.validate(m, SCHEMA)
    else:
        with pytest.raises(jsonschema.ValidationError):
            jsonschema.validate(m, SCHEMA)
# tests/test_packer_reproducible.py
def test_pack_is_reproducible(tmp_path):
    m = tmp_path / "manifest.json"; m.write_text(MINIMAL_MANIFEST)
    r1 = pack(m, out_dir=tmp_path / "a")
    r2 = pack(m, out_dir=tmp_path / "b")
    assert r1.sha256 == r2.sha256

def test_pack_epoch_mtime(tmp_path):
    r = pack(_minimal(tmp_path), out_dir=tmp_path / "out")
    import zipfile
    with zipfile.ZipFile(r.bundle_path) as zf:
        for info in zf.infolist():
            assert info.date_time == (1980, 1, 1, 0, 0, 0)
# tests/test_packer_signature_required.py
def test_pack_refuses_unsigned(tmp_path):
    m = tmp_path / "m.json"
    raw = json.loads(MINIMAL_MANIFEST); del raw["signature"]; m.write_text(json.dumps(raw))
    with pytest.raises(UnsignedManifestError):
        pack(m, fail_on_unsigned=True)
# tests/test_cli_pack_smoke.sh
cyberos-plugin pack modules/plugin/manifests/cyberos@1.0.0.plugin.json \
    --target claude-code --out /tmp/pack-test
test -f /tmp/pack-test/cyberos-1.0.0.plugin
cyberos-plugin doctor /tmp/pack-test/cyberos-1.0.0.plugin
echo $?  # → 0

§6 — Implementation skeleton

(API contract above is the skeleton — packer.py, validator.py, reproducible.py, cli.py are thin orchestrators around jsonschema.validate + zipfile.ZipFile. The custom validator additions live in validator.py::check_capability_scope_coherence.)


§7 — Dependencies


§8 — Example payloads

Valid minimal manifest (see §3 for content)

Validation error example

{
  "errors": [
    {
      "json_pointer": "/tools/0/name",
      "constraint": "pattern",
      "expected": "^cyberos\\.[a-z][a-z0-9]*\\.[a-z][a-z0-9_]*$",
      "actual": "execute_workflow",
      "message": "Tool name 'execute_workflow' must follow SEP-986: 'cyberos.{module}.{verb}_{noun}'. Per TASK-MCP-003."
    },
    {
      "json_pointer": "/signature/rekor_uuid",
      "constraint": "required",
      "message": "Missing Rekor transparency-log UUID. Bundles must be Sigstore-signed per TASK-PLUGIN-001 §1 clause 8."
    }
  ]
}

Pack result

{
  "bundle_path": "dist/cyberos-1.0.0.plugin",
  "sha256": "a1b2c3d4e5f6...",
  "size_bytes": 184320,
  "tools_count": 8,
  "commands_count": 4,
  "skills_count": 12
}

§9 — Open questions

All resolved.


§10 — Failure modes inventory

FailureDetectionOutcomeRecovery
Manifest missing required fieldJSONSchema validationcyberos-plugin pack exits 1 with /<field>: requiredAuthor adds field
Tool name violates SEP-986JSONSchema patternexit 1 with /tools/N/name: patternAuthor renames per cyberos.{module}.{verb}_{noun}
schema_version mismatchJSONSchema constexit 1 with /schema_version: constAuthor downgrades to "1.0.0" or waits for task-PLUGIN-001a
Capability declared without matching scopevalidator.py custom checkexit 1 with capability '<x>' declared but no tool has scope '<y>'Author adds the scope to one of the tools
Destructive tool without write scopevalidator.py custom checkexit 1 with tool '<n>' marked destructive but lacks write scopeAuthor adds the write scope OR un-marks the annotation
Unsigned manifest at pack timepacker.py checkraises UnsignedManifestErrorAuthor runs Sigstore sign first, then re-packs
Reproducibility broken by clock-injection bugCI: two consecutive pack() calls compare SHA-256CI failsInvestigate which file source injects mtime; remove
Adapter for --target not yet implementedpack(target="goose") in P1raises UnknownTargetErrorUser picks a P1 target or waits for P2
Bundle SHA-256 mismatch on verifier rebuildSigstore verifier rebuilds bundle, compares hashverifier rejectsInvestigate non-determinism in packer; report bug
Mtimes injected by network filesystempack() on NFS sees host clocksreproducible check failsUse local fs for packing; CI runs on local tmpfs
Marketplace upload of non-validated bundlecyberos-plugin publish re-runs validate before uploadexit 1Author re-runs pack with valid manifest
Future schema field appears in v1 manifestadditionalProperties: falseexit 1 with unknown propertyField MUST land via task-PLUGIN-001a, not in v1
Empty tools arraytools.minItems: 1exit 1A plugin with zero tools is meaningless; add at least one
Description shorter than 60 charsdescription.minLength: 60exit 1Author writes meaningful description
Tool input_schema not a JSONSchema objectJSONSchema typeexit 1Author fixes input_schema
Cross-platform path separator in commands/filevalidator: enforces forward slashesexit 1Author uses POSIX paths in manifest

§11 — Implementation notes


End of TASK-PLUGIN-001 spec.