mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 20:38:31 +00:00
chore: address plugin manifest review
Signed-off-by: barlind <tobias@barlind.se>
This commit is contained in:
@@ -30,7 +30,7 @@ If the plugin slug or type is missing, ask once.
|
|||||||
|
|
||||||
**Common to all types:**
|
**Common to all types:**
|
||||||
|
|
||||||
- `plugins/<id>/plugin.json` — schema-valid manifest. Set `id`, `name`, `version: "0.1.0"`, `license: "AGPL-3.0-only"`, and `standards: ["capability-pipelines.v1", "plugin-runtime-idempotent.v1"]` by default (ask if a different license is desired).
|
- `plugins/<id>/plugin.json` — schema-valid manifest. Set `id`, `name`, `version: "0.1.0"`, `license: "AGPL-3.0-only"`, and `standards: ["capability-pipelines.v1", "plugin-runtime-idempotent.v1"]` by default.
|
||||||
|
|
||||||
**`type=visualization`** — adds:
|
**`type=visualization`** — adds:
|
||||||
- `"type": "visualization"` and `"script": "screen.js"` to manifest
|
- `"type": "visualization"` and `"script": "screen.js"` to manifest
|
||||||
|
|||||||
@@ -34,5 +34,5 @@ Thanks for the PR! A few quick reminders before you hit submit:
|
|||||||
|
|
||||||
- [ ] DCO sign-off on every commit (`Signed-off-by:` trailer)
|
- [ ] DCO sign-off on every commit (`Signed-off-by:` trailer)
|
||||||
- [ ] Conventional-commit subject (`feat(scope):`, `fix(scope):`, `docs:`, `chore:`)
|
- [ ] Conventional-commit subject (`feat(scope):`, `fix(scope):`, `docs:`, `chore:`)
|
||||||
- [ ] `CHANGELOG.md` `[Unreleased]` section updated (skip for chore/docs)
|
- [ ] `CHANGELOG.md` `[Unreleased]` section updated
|
||||||
- [ ] Documentation updated if behaviour or contracts changed
|
- [ ] Documentation updated if behaviour or contracts changed
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ Supported declaration fields include:
|
|||||||
- `roles`: `owner`, `coordinator`, `provider`, `observer`, `requester`, `transformer`, `handler`, `validator`, `short-circuiter`, `contributor`
|
- `roles`: `owner`, `coordinator`, `provider`, `observer`, `requester`, `transformer`, `handler`, `validator`, `short-circuiter`, `contributor`
|
||||||
- `commands`, `operations`, `requests`, `observes`, `emits`, `events`: string arrays naming public commands, provider operations, or events
|
- `commands`, `operations`, `requests`, `observes`, `emits`, `events`: string arrays naming public commands, provider operations, or events
|
||||||
- `kind`: `command`, `provider-coordinator`, `event`, `diagnostic`, `privileged`
|
- `kind`: `command`, `provider-coordinator`, `event`, `diagnostic`, `privileged`
|
||||||
- `mode`: `active`, `optional`, `disabled`
|
- `mode`: `active`, `optional`, `legacy-shim`, `disabled`
|
||||||
- `compatibility`: prefer `none` for new declarations
|
- `compatibility`: prefer `none` for new declarations
|
||||||
- `ownership`: `exclusive-owner`, `multi-provider`, `observer-only`, `requester-only`, `privileged`, `diagnostic-only`
|
- `ownership`: `exclusive-owner`, `multi-provider`, `observer-only`, `requester-only`, `privileged`, `diagnostic-only`
|
||||||
- `safety`: `safe`, `privileged`, `sensitive`, `diagnostic-only`
|
- `safety`: `safe`, `privileged`, `sensitive`, `diagnostic-only`
|
||||||
@@ -191,7 +191,7 @@ Invalid capability metadata is rejected by schema validation and ignored by runt
|
|||||||
|
|
||||||
### `license` (string, optional but recommended)
|
### `license` (string, optional but recommended)
|
||||||
|
|
||||||
SPDX identifier. For curated plugins, must be AGPL-3.0-or-later or AGPL-compatible (MIT, BSD-2-Clause, BSD-3-Clause, Apache-2.0). See [CONTRIBUTING.md](../CONTRIBUTING.md).
|
SPDX identifier. Contributions must use `AGPL-3.0-only`. See [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||||
|
|
||||||
## Backend routes — `setup(app, context)`
|
## Backend routes — `setup(app, context)`
|
||||||
|
|
||||||
|
|||||||
@@ -95,15 +95,41 @@
|
|||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"nav": {},
|
"nav": {},
|
||||||
"screen": {},
|
"screen": {
|
||||||
"script": {},
|
"$ref": "#/$defs/pluginRelpath"
|
||||||
"routes": {},
|
},
|
||||||
"settings": {},
|
"script": {
|
||||||
|
"$ref": "#/$defs/pluginRelpath"
|
||||||
|
},
|
||||||
|
"routes": {
|
||||||
|
"$ref": "#/$defs/pluginRelpath"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"html": {
|
||||||
|
"$ref": "#/$defs/pluginRelpath"
|
||||||
|
},
|
||||||
|
"server_files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"not": {
|
||||||
|
"pattern": "^/|^[a-zA-Z]:|\\\\|(^|/)\\.\\.(/|$)|//|(^|/)\\.(/|$)|^\\."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"diagnostics": {},
|
"diagnostics": {},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"tour": {},
|
"tour": {
|
||||||
|
"$ref": "#/$defs/pluginRelpath"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Short one-sentence summary of the plugin, surfaced on the v3 Pedalboard Plugins page (clamped to ~2 lines). Optional and additive."
|
"description": "Short one-sentence summary of the plugin, surfaced on the v3 Pedalboard Plugins page (clamped to ~2 lines). Optional and additive."
|
||||||
@@ -127,6 +153,13 @@
|
|||||||
},
|
},
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"$defs": {
|
"$defs": {
|
||||||
|
"pluginRelpath": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"not": {
|
||||||
|
"pattern": "^/|^[a-zA-Z]:|\\\\|(^|/)\\.\\.(/|$)|//|(^|/)\\.(/|$)|^\\.|[?#]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"domainName": {
|
"domainName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
|
|||||||
+13
-23
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://github.com/slopsmith/slopsmith/blob/main/schema/plugin.schema.json",
|
"$id": "https://github.com/slopsmith/slopsmith/blob/main/schema/plugin.schema.json",
|
||||||
"title": "Slopsmith plugin manifest",
|
"title": "Slopsmith plugin manifest",
|
||||||
"description": "Schema for plugins/<id>/plugin.json. See docs/plugin-manifest.md for prose. License enum mirrors the curated-plugin allowlist in CONTRIBUTING.md (keep them in sync; tests/test_plugin_schema.py asserts subset). Capability metadata follows capability-pipelines.v1 and is intentionally additive.",
|
"description": "Schema for plugins/<id>/plugin.json. See docs/plugin-manifest.md for prose. Contributions must use AGPL-3.0-only. Capability metadata follows capability-pipelines.v1 and is intentionally additive.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"required": ["id", "name"],
|
"required": ["id", "name"],
|
||||||
@@ -51,23 +51,8 @@
|
|||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "SPDX identifier. For curated plugins, must match CONTRIBUTING.md's allowlist.",
|
"description": "SPDX identifier. Contributions must use AGPL-3.0-only.",
|
||||||
"enum": [
|
"enum": ["AGPL-3.0-only"]
|
||||||
"AGPL-3.0-only",
|
|
||||||
"AGPL-3.0-or-later",
|
|
||||||
"GPL-3.0-only",
|
|
||||||
"GPL-3.0-or-later",
|
|
||||||
"LGPL-3.0-only",
|
|
||||||
"LGPL-3.0-or-later",
|
|
||||||
"MIT",
|
|
||||||
"BSD-2-Clause",
|
|
||||||
"BSD-3-Clause",
|
|
||||||
"Apache-2.0",
|
|
||||||
"ISC",
|
|
||||||
"Unlicense",
|
|
||||||
"CC0-1.0",
|
|
||||||
"0BSD"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -80,11 +65,11 @@
|
|||||||
"description": "Optional navigation metadata. Plugin UI declares stable ui contributions for attribution."
|
"description": "Optional navigation metadata. Plugin UI declares stable ui contributions for attribution."
|
||||||
},
|
},
|
||||||
"screen": {
|
"screen": {
|
||||||
"type": "string",
|
"$ref": "#/$defs/pluginRelpath",
|
||||||
"description": "Relative path to HTML mounted at #plugin-<id>."
|
"description": "Relative path to HTML mounted at #plugin-<id>."
|
||||||
},
|
},
|
||||||
"script": {
|
"script": {
|
||||||
"type": "string",
|
"$ref": "#/$defs/pluginRelpath",
|
||||||
"description": "Relative path to JS loaded in global scope on page load. Wrap your code in an IIFE."
|
"description": "Relative path to JS loaded in global scope on page load. Wrap your code in an IIFE."
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
@@ -94,11 +79,11 @@
|
|||||||
"description": "Plugin-root-relative path under assets/ (e.g. 'assets/plugin.css') to a compiled, preflight-off Tailwind stylesheet the frontend injects as a <link>. Must stay under assets/ with no '..', backslash, query, or fragment. See docs/plugin-styles.md."
|
"description": "Plugin-root-relative path under assets/ (e.g. 'assets/plugin.css') to a compiled, preflight-off Tailwind stylesheet the frontend injects as a <link>. Must stay under assets/ with no '..', backslash, query, or fragment. See docs/plugin-styles.md."
|
||||||
},
|
},
|
||||||
"routes": {
|
"routes": {
|
||||||
"type": "string",
|
"$ref": "#/$defs/pluginRelpath",
|
||||||
"description": "Relative path to Python module exporting setup(app, context). See docs/plugin-manifest.md."
|
"description": "Relative path to Python module exporting setup(app, context). See docs/plugin-manifest.md."
|
||||||
},
|
},
|
||||||
"tour": {
|
"tour": {
|
||||||
"type": "string",
|
"$ref": "#/$defs/pluginRelpath",
|
||||||
"description": "Relative path to tour JSON for the in-app onboarding tour."
|
"description": "Relative path to tour JSON for the in-app onboarding tour."
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -106,7 +91,7 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"html": {
|
"html": {
|
||||||
"type": "string",
|
"$ref": "#/$defs/pluginRelpath",
|
||||||
"description": "Relative path to settings-panel HTML."
|
"description": "Relative path to settings-panel HTML."
|
||||||
},
|
},
|
||||||
"server_files": {
|
"server_files": {
|
||||||
@@ -195,6 +180,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"$defs": {
|
"$defs": {
|
||||||
|
"pluginRelpath": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"not": { "pattern": "^/|^[a-zA-Z]:|\\\\|(^|/)\\.\\.(/|$)|//|(^|/)\\.(/|$)|^\\.|[?#]" }
|
||||||
|
},
|
||||||
"domainName": {
|
"domainName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
|
|||||||
+34
-48
@@ -7,12 +7,8 @@ Four independent guarantees:
|
|||||||
2. Each in-tree manifest's `id` matches its parent directory name —
|
2. Each in-tree manifest's `id` matches its parent directory name —
|
||||||
the loader assumes this and silent drift would break plugin
|
the loader assumes this and silent drift would break plugin
|
||||||
discovery.
|
discovery.
|
||||||
3. The `license` enum in the schema is a subset of the SPDX identifiers
|
3. The `license` enum in the schema enforces AGPL-3.0-only for
|
||||||
listed in `CONTRIBUTING.md`'s "Plugin licensing" curated allowlist.
|
contributed manifests.
|
||||||
If you edit the allowlist in `CONTRIBUTING.md`, run pytest locally
|
|
||||||
and update the schema enum to match — these two files must stay in
|
|
||||||
sync because the same allowlist is referenced from both human-facing
|
|
||||||
docs and from CI manifest validation.
|
|
||||||
4. The schema accepts capability-pipelines.v1 manifest metadata so
|
4. The schema accepts capability-pipelines.v1 manifest metadata so
|
||||||
native capability declarations stay first-class in tooling.
|
native capability declarations stay first-class in tooling.
|
||||||
"""
|
"""
|
||||||
@@ -69,6 +65,7 @@ def test_schema_contains_capability_contract(schema: dict) -> None:
|
|||||||
assert "diagnostic-only" in declaration["properties"]["safety"]["enum"]
|
assert "diagnostic-only" in declaration["properties"]["safety"]["enum"]
|
||||||
assert "styles" in schema["properties"]
|
assert "styles" in schema["properties"]
|
||||||
assert schema["properties"]["styles"]["pattern"].startswith("^assets/")
|
assert schema["properties"]["styles"]["pattern"].startswith("^assets/")
|
||||||
|
assert "pluginRelpath" in schema["$defs"]
|
||||||
|
|
||||||
|
|
||||||
def test_docs_schema_capability_contract_matches_ci_schema(schema: dict, docs_schema: dict) -> None:
|
def test_docs_schema_capability_contract_matches_ci_schema(schema: dict, docs_schema: dict) -> None:
|
||||||
@@ -80,8 +77,9 @@ def test_docs_schema_capability_contract_matches_ci_schema(schema: dict, docs_sc
|
|||||||
return [without_descriptions(item) for item in value]
|
return [without_descriptions(item) for item in value]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
for key in ("standards", "capability_api", "capabilities", "ui", "ui_contributions", "runtime_domains", "domains", "settings_schema", "styles"):
|
for key in ("standards", "capability_api", "capabilities", "ui", "ui_contributions", "runtime_domains", "domains", "settings_schema", "styles", "screen", "script", "routes", "tour", "settings"):
|
||||||
assert without_descriptions(docs_schema["properties"][key]) == without_descriptions(schema["properties"][key])
|
assert without_descriptions(docs_schema["properties"][key]) == without_descriptions(schema["properties"][key])
|
||||||
|
assert without_descriptions(docs_schema["$defs"]["pluginRelpath"]) == without_descriptions(schema["$defs"]["pluginRelpath"])
|
||||||
assert without_descriptions(docs_schema["$defs"]["domainName"]) == without_descriptions(schema["$defs"]["domainName"])
|
assert without_descriptions(docs_schema["$defs"]["domainName"]) == without_descriptions(schema["$defs"]["domainName"])
|
||||||
assert without_descriptions(docs_schema["$defs"]["capabilityDeclaration"]) == without_descriptions(schema["$defs"]["capabilityDeclaration"])
|
assert without_descriptions(docs_schema["$defs"]["capabilityDeclaration"]) == without_descriptions(schema["$defs"]["capabilityDeclaration"])
|
||||||
assert without_descriptions(docs_schema["$defs"]["domainDeclaration"]) == without_descriptions(schema["$defs"]["domainDeclaration"])
|
assert without_descriptions(docs_schema["$defs"]["domainDeclaration"]) == without_descriptions(schema["$defs"]["domainDeclaration"])
|
||||||
@@ -148,6 +146,7 @@ def test_capability_manifest_metadata_validates(schema: dict) -> None:
|
|||||||
"id": "capability_example",
|
"id": "capability_example",
|
||||||
"name": "Capability Example",
|
"name": "Capability Example",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"license": "AGPL-3.0-only",
|
||||||
"standards": ["capability-pipelines.v1", "plugin-runtime-idempotent.v1"],
|
"standards": ["capability-pipelines.v1", "plugin-runtime-idempotent.v1"],
|
||||||
"script": "screen.js",
|
"script": "screen.js",
|
||||||
"settings": {"html": "settings.html"},
|
"settings": {"html": "settings.html"},
|
||||||
@@ -214,48 +213,35 @@ def test_invalid_capability_metadata_fails_schema(schema: dict) -> None:
|
|||||||
jsonschema.validate(manifest, schema)
|
jsonschema.validate(manifest, schema)
|
||||||
|
|
||||||
|
|
||||||
def _extract_allowlist_from_contributing() -> set[str]:
|
def test_schema_license_enum_requires_agpl_only(schema: dict) -> None:
|
||||||
"""Pull the curated-license allowlist out of CONTRIBUTING.md.
|
"""Contributed manifests must not validate with non-AGPL licenses."""
|
||||||
|
assert schema["properties"]["license"]["enum"] == ["AGPL-3.0-only"]
|
||||||
|
|
||||||
Looks at the "Plugin licensing" section: any bullet line whose text
|
manifest = {"id": "license_example", "name": "License Example", "license": "MIT"}
|
||||||
starts with a recognized SPDX-shape identifier is considered part of
|
with pytest.raises(jsonschema.ValidationError):
|
||||||
the allowlist. Forms like "AGPL-3.0-only or AGPL-3.0-or-later" are
|
jsonschema.validate(manifest, schema)
|
||||||
split on " or ".
|
|
||||||
"""
|
|
||||||
text = CONTRIBUTING_PATH.read_text(encoding="utf-8")
|
|
||||||
section = text.split("## Plugin licensing", 1)
|
|
||||||
if len(section) < 2:
|
|
||||||
pytest.fail("'## Plugin licensing' section not found in CONTRIBUTING.md")
|
|
||||||
body = section[1].split("\n## ", 1)[0]
|
|
||||||
|
|
||||||
spdx_re = re.compile(r"^[A-Za-z0-9.+-]+$")
|
|
||||||
allowlist: set[str] = set()
|
|
||||||
for line in body.splitlines():
|
|
||||||
if not line.lstrip().startswith("- "):
|
|
||||||
continue
|
|
||||||
rest = line.lstrip()[2:].strip()
|
|
||||||
# Strip trailing punctuation / parenthetical notes.
|
|
||||||
rest = re.split(r"\s*\(|\s*—|\s*--", rest)[0].strip().rstrip(".,;")
|
|
||||||
for token in re.split(r"\s+or\s+|\s*/\s+|\s*,\s+", rest):
|
|
||||||
token = token.strip().rstrip(".,;").strip()
|
|
||||||
if token and spdx_re.match(token):
|
|
||||||
allowlist.add(token)
|
|
||||||
return allowlist
|
|
||||||
|
|
||||||
|
|
||||||
def test_schema_license_enum_subset_of_contributing_allowlist(schema: dict) -> None:
|
def test_plugin_runtime_paths_are_plugin_relative(schema: dict) -> None:
|
||||||
"""Schema's license enum must be ⊆ CONTRIBUTING.md curated allowlist.
|
"""Runtime file path fields must reject escapes and URL suffixes."""
|
||||||
|
valid = {
|
||||||
|
"id": "path_example",
|
||||||
|
"name": "Path Example",
|
||||||
|
"screen": "screen.html",
|
||||||
|
"script": "assets/screen.js",
|
||||||
|
"routes": "routes.py",
|
||||||
|
"tour": "tours/intro.json",
|
||||||
|
"settings": {"html": "settings/settings.html"},
|
||||||
|
}
|
||||||
|
jsonschema.validate(valid, schema)
|
||||||
|
|
||||||
If you add a license to the schema enum, also list it in
|
for field in ("screen", "script", "routes", "tour"):
|
||||||
CONTRIBUTING.md "Plugin licensing". Direction matters: schema ⊆
|
for bad_path in ("../escape.html", "safe/../escape.html", "/abs.html", "C:/abs.html", "dir\\file.js", "screen.html?x=1", "screen.html#frag", "./screen.html", ".hidden"):
|
||||||
allowlist (the schema can be stricter than what CONTRIBUTING.md
|
manifest = {"id": "bad_path_example", "name": "Bad Path Example", field: bad_path}
|
||||||
documents — typically the schema *equals* the allowlist).
|
with pytest.raises(jsonschema.ValidationError):
|
||||||
"""
|
jsonschema.validate(manifest, schema)
|
||||||
license_enum = set(schema["properties"]["license"]["enum"])
|
|
||||||
allowlist = _extract_allowlist_from_contributing()
|
for bad_path in ("../settings.html", "settings/../settings.html", "/settings.html", "settings\\settings.html", "settings.html?x=1", "settings.html#frag", "./settings.html", ".settings.html"):
|
||||||
missing = license_enum - allowlist
|
manifest = {"id": "bad_settings_path", "name": "Bad Settings Path", "settings": {"html": bad_path}}
|
||||||
assert not missing, (
|
with pytest.raises(jsonschema.ValidationError):
|
||||||
f"License enum values present in schema/plugin.schema.json but "
|
jsonschema.validate(manifest, schema)
|
||||||
f"not listed in CONTRIBUTING.md 'Plugin licensing' section: {sorted(missing)}. "
|
|
||||||
f"Update CONTRIBUTING.md or remove from the schema enum."
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user