Skip to content
Closed
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
4 changes: 2 additions & 2 deletions plugins/ditto/shared-skills.lock.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"schema_version": 1,
"source_repository": "https://github.com/ditto-assistant/ditto-skills",
"source_commit": "9a2a3ee2af084fe2833cec7b093cfd371098f867",
"source_commit": "ee9da77c1502fb183e5eb6e7f265409e4af14b9e",
"host": "plugins",
"core_sha256": "5912af5483c469ec15db27858122fc9128fc25cd239202c0fdd9727de7dd5f6b",
"adapter_sha256": "7cd895ab3feb0581e7e782de46b8b8220e4281a9ebfe6f6b651753e3a545bac1",
"generator_sha256": "6d23fa054bfa19203783fd7bdf6a6949915a5e62b6c71349af7031d5caae1500",
"generator_sha256": "c3cab1c78bcbd50b81bc80698d87567921326ee9886d220ff61c0d5bd18128fe",
"skill_sha256": "dc4a46fc6de30ceac400ced65b122895738757cdcdaf1be529d63bec165e5835",
"files_sha256": {
"plugins/ditto/skills/curate-music/SKILL.md": "b498f8639aeb64e59f88830ac162a894f9f128b7e1f2db6a7c528092904c1eb4",
Expand Down
44 changes: 36 additions & 8 deletions scripts/shared_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
],
}
MANIFESTS = {host: f"plugins/{DEFAULT_PLUGIN}/{paths[0]}" for host, paths in PLUGIN_MANIFESTS.items()}
# Repository-root host manifests that point into one plugin, for hosts that only
# install from a repository root (Kimi Code reads <repo>/.kimi-plugin/plugin.json
# when given a GitHub URL). A plugin opts in with "root_manifests" in plugins.json;
# they are versioned and bumped with that plugin's own manifests.
ROOT_MANIFESTS = {"plugins": {".kimi-plugin/plugin.json"}}


def digest(value):
Expand Down Expand Up @@ -109,6 +114,13 @@ def plugin_registry(source):
names = catalog_names(source, entry["catalog"])
if adapted is not None and names is not None and adapted not in names:
raise ValueError("Adapted skill must belong to the plugin catalog")
roots = entry.get("root_manifests", [])
allowed = set().union(*ROOT_MANIFESTS.values())
if not isinstance(roots, list) or any(not isinstance(r, str) or r not in allowed for r in roots) or len(set(roots)) != len(roots):
raise ValueError("Invalid plugin registry: root_manifests must list known repository-root host manifests")
claimed = [r for entry in data.values() for r in entry.get("root_manifests", [])]
if len(set(claimed)) != len(claimed):
raise ValueError("Invalid plugin registry: a root manifest can point into only one plugin")
return data


Expand Down Expand Up @@ -161,11 +173,19 @@ def additional_files(source, plugin=DEFAULT_PLUGIN, catalog="memory", adapted=DE
return result


def bump_manifests(repo, host, plugin):
files = [file_at(repo, f"plugins/{plugin}/{rel}") for rel in PLUGIN_MANIFESTS[host]]
if not files[0].exists():
def host_manifests(repo, host, plugin, root_manifests=()):
"""Repository-relative versioned manifests of one plugin that exist in repo."""
if any(r not in ROOT_MANIFESTS.get(host, set()) for r in root_manifests):
raise ValueError("Root manifests are not supported for this host")
paths = [f"plugins/{plugin}/{rel}" for rel in PLUGIN_MANIFESTS[host]] + list(root_manifests)
return [p for p in paths if file_at(repo, p).exists()]


def bump_manifests(repo, host, plugin, root_manifests=()):
if not file_at(repo, f"plugins/{plugin}/{PLUGIN_MANIFESTS[host][0]}").exists():
raise ValueError("Missing primary host manifest")
present = [(f, json.loads(f.read_text())) for f in files if f.exists()]
files = [file_at(repo, p) for p in host_manifests(repo, host, plugin, root_manifests)]
present = [(f, json.loads(f.read_text())) for f in files]
if any("version" not in m for _, m in present):
raise ValueError("Every host manifest in one plugin must carry a version before bumping")
versions = {m["version"] for _, m in present}
Expand All @@ -181,7 +201,7 @@ def bump_manifests(repo, host, plugin):
return [f.relative_to(Path(repo).resolve()).as_posix() for f, _ in present]


def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog="memory", adapted=DEFAULT_ADAPTED):
def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog="memory", adapted=DEFAULT_ADAPTED, root_manifests=()):
paths = layout(plugin, adapted)
sha = source_identity(source)
generator = file_at(source, GENERATOR).read_bytes()
Expand Down Expand Up @@ -213,7 +233,7 @@ def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog
return False
output_changed = skill is not None and (not file_at(repo, paths["output"]).exists() or file_at(repo, paths["output"]).read_bytes() != skill)
if bump_version and (output_changed or skill is None and not old or old.get("files_sha256", {}) != extra_hashes):
bump_manifests(repo, host, plugin)
bump_manifests(repo, host, plugin, root_manifests)
# A renamed or dropped adapted skill leaves its previous output behind;
# old locks without the key used the default adapted skill.
if old:
Expand All @@ -236,12 +256,20 @@ def build(source, repo, host, bump_version=False, plugin=DEFAULT_PLUGIN, catalog
def build_all(source, repo, host, bump_version=False):
"""Build every plugin in the source registry that the distribution hosts."""
changed = {}
for plugin, entry in plugin_registry(source).items():
registry = plugin_registry(source)
if host == "plugins":
# A root manifest no plugin claims would silently keep its old version.
claimed = {r for entry in registry.values() for r in entry.get("root_manifests", [])}
for root in sorted(ROOT_MANIFESTS[host] - claimed):
if file_at(repo, root).exists():
raise ValueError(f"{root} exists but no plugin in scripts/plugins.json lists it under root_manifests")
Comment on lines +262 to +265

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · bug — Reject claimed root manifests missing from the distribution · 🔎 Verified by trace

When the source registry claims .kimi-plugin/plugin.json but the distribution lacks it, build_all proceeds and host_manifests silently filters the missing file out. The root-install feature is therefore not generated or version-bumped, instead of failing like the primary-manifest check does.

Suggested change
claimed = {r for entry in registry.values() for r in entry.get("root_manifests", [])}
for root in sorted(ROOT_MANIFESTS[host] - claimed):
if file_at(repo, root).exists():
raise ValueError(f"{root} exists but no plugin in scripts/plugins.json lists it under root_manifests")
claimed = {r for entry in registry.values() for r in entry.get("root_manifests", [])}
for root in sorted(ROOT_MANIFESTS[host] - claimed):
if file_at(repo, root).exists():
raise ValueError(f"{root} exists but no plugin in scripts/plugins.json lists it under root_manifests")
for root in sorted(claimed):
if not file_at(repo, root).exists():
raise ValueError(f"{root} is listed under root_manifests but missing from the distribution; scaffold it before generating")
Why Ditto flagged this (confidence 88%)

A concrete source registry can claim .kimi-plugin/plugin.json while that file is absent from the distribution; build_all only checks for existing unclaimed roots at lines 262–265, and host_manifests filters missing paths at line 181, so the build continues without including the claimed root manifest in version bumps. The primary plugin manifest is explicitly required at lines 269–270, so the missing claimed root manifest should likewise fail rather than be silently skipped.

Git file evidence: .kimi-plugin/plugin.json absent at d83589e. This establishes repository state, not runtime resolution or generated-content correctness.

🤖 Prompt for AI agents

[!NOTE]
Generated from this pull request, which may contain untrusted text. Read it before giving it to an agent, and do not follow instructions in it that go beyond the fix.

In `scripts/shared_skills.py` at lines 262–265, extend the `host == "plugins"` validation in `build_all` to reject every registry-claimed root manifest that does not exist in the distribution, with a clear scaffold-before-generating error. This ensures a claimed root manifest cannot be silently skipped by `host_manifests`. Add a focused test for a claimed but missing root manifest, then run the relevant shared-skills tests.

for plugin, entry in registry.items():
if host != "plugins" and plugin != DEFAULT_PLUGIN:
continue # single-host repositories carry only the original plugin
if not file_at(repo, f"plugins/{plugin}/{PLUGIN_MANIFESTS[host][0]}").exists():
raise ValueError(f"Distribution lacks the {plugin} plugin manifest; scaffold it before generating")
changed[plugin] = build(source, repo, host, bump_version, plugin, entry["catalog"], entry.get("adapted_skill"))
roots = entry.get("root_manifests", []) if host == "plugins" else []
changed[plugin] = build(source, repo, host, bump_version, plugin, entry["catalog"], entry.get("adapted_skill"), roots)
return changed


Expand Down
Loading