Skip to content

fix(cli): preserve trust for plugin-provided skills - #12760

Closed
noobezlol wants to merge 5 commits into
Kilo-Org:mainfrom
noobezlol:agent/fix-plugin-skill-origins
Closed

fix(cli): preserve trust for plugin-provided skills#12760
noobezlol wants to merge 5 commits into
Kilo-Org:mainfrom
noobezlol:agent/fix-plugin-skill-origins

Conversation

@noobezlol

Copy link
Copy Markdown
Contributor

Issue

Fixes #12468

What changed

Plugin config hooks can add skill directories after the normal config loader has recorded skill-path provenance. Those paths were subsequently treated as untrusted project content, so plugin skills outside the workspace could fail the markdown scope check and disappear with only a generic parse error.

The plugin initialization step now records provenance for skill paths added by config hooks and marks them trusted. A regression test loads a skill from a plugin-provided directory outside the project root.

Validation

  • npx --yes bun@1.3.14 test test/skill/skill.test.ts test/kilocode/plugin-skill-path.test.ts
  • npx --yes bun@1.3.14 run typecheck
  • Prettier check and git diff --check

@noobezlol
noobezlol marked this pull request as ready for review August 1, 2026 10:52
// project-root sandbox to its external package directory.
const origins = { ...cfg.skill_path_origins }
for (const path of paths) {
if (origins[path]) continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: Trust here is inferred from a missing origin, which fails open

The heuristic "no recorded origin => added by a plugin => trusted" holds for today's code (every config source flows through merge() in config/config.ts, which records skill_path_origins), but it is a fail-open default in the trust map added by the markdown-exfiltration fix (#12168). Any future code path that appends to cfg.skills.paths without recording provenance — a server route, an injected config, an upstream merge — silently becomes trusted: true, which re-enables {env:}/{file:} substitution and shell execution for that directory.

A precise alternative that keeps the same behavior without the assumption: snapshot the paths before the config-hook loop in plugin/index.ts and pass the delta, e.g. const before = new Set(cfg.skills?.paths ?? []) before the loop and PluginSkillOrigins.mark(cfg, before) after, marking only paths not in before. That way only paths actually added by a hook get plugin trust.

Related nit: a plugin that contributes a relative path still ends up untrusted, because skill/index.ts:274 requires path.isAbsolute(expanded). Worth a comment if that is intentional.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

init: (dir) =>
Effect.promise(() =>
Bun.write(
path.join(path.dirname(dir), "plugin-skills", "example", "SKILL.md"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SUGGESTION: Fixture writes to a fixed path in the system temp root and never cleans it up

dir is os.tmpdir()/opencode-test-<random> (see test/fixture/fixture.ts:153), so path.dirname(dir) is the system temp root and this creates ${TMPDIR}/plugin-skills/example/SKILL.md. The scoped finalizer only removes dir, so the directory leaks and is shared by every run and every concurrent shard. A leftover copy from a previous run also means the test can pass even if this write silently fails.

A unique sibling directory keeps the "outside the project root" property without the shared name, e.g. path.join(`${dir}-plugin-skills`, "example", "SKILL.md") here plus the matching `${input.directory}-plugin-skills` in test/kilocode/fixtures/plugin-skill.ts:8.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const list = yield* Skill.Service.use((service) => service.all())

expect(list.find((item) => item.name === "plugin-skill")).toMatchObject({
description: "Registered by a plugin config hook.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SUGGESTION: Assert the trust flag, not just discovery

The fix is specifically about provenance/trust, but the assertion only checks that the skill was discovered. Adding trusted: true to the matched object pins the actual behavior being fixed, so a future regression that discovers the skill as untrusted (which changes substitution and shell-execution behavior) still fails the test.

Suggested change
description: "Registered by a plugin config hook.",
description: "Registered by a plugin config hook.",
trusted: true,

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/test/kilocode/plugin-skill-path.test.ts 56 Assertion still only checks discovery and content substitution; Skill.Info now carries trusted, so asserting trusted: true would pin the exact behavior this fix restores.
Files Reviewed (5 files)
  • .changeset/trusted-plugin-skills.md - 0 issues
  • packages/opencode/src/kilocode/config/plugin-skill-origins.ts - 0 issues
  • packages/opencode/src/plugin/index.ts - 0 issues
  • packages/opencode/test/kilocode/fixtures/plugin-skill.ts - 0 issues
  • packages/opencode/test/kilocode/plugin-skill-path.test.ts - 1 issue

Incremental review of c2b791b..1ee08fd. That range contains only a main merge plus an empty retrigger commit — no new PR code. Re-verified the fix against the merged main, which now adds kilocode/skill/trust.ts and Skill.Info.trusted: mark() still complements Config.load provenance rather than duplicating it, and a plugin-provided path that resolves inside the project still loses trust in scan via trustedInProject, so the fail-closed boundary holds. Fork hygiene remains minimal (two annotated lines in src/plugin/index.ts). Tests were not executed (read-only review).

Fix these issues in Kilo Cloud

Previous Review Summaries (3 snapshots, latest commit c2b791b)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit c2b791b)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/test/kilocode/plugin-skill-path.test.ts 56 Assertion still only checks discovery and content substitution; asserting trusted: true (or the origin entry) would pin the trust behavior this fix is about.
Files Reviewed (3 files)
  • packages/opencode/src/plugin/index.ts - 0 issues
  • packages/opencode/test/kilocode/fixtures/plugin-skill.ts - 0 issues
  • packages/opencode/test/kilocode/plugin-skill-path.test.ts - 1 issue

Incremental review of 1c2de7d..c2b791b. No new issues. Resolved since the last review: the shared upstream local is now the single-word before, the unused path import is gone from the fixture, and the sibling ${dir}-plugin-skills tree is cleaned up via Effect.addFinalizer inside init — that finalizer is registered on the test's outer scope (tmpdirScoped runs init under the scope closed by Effect.scoped in test/lib/effect.ts), so the directory survives the test body and is removed afterwards. Fork hygiene remains good: the shared file still only carries a two-line annotated hook. Tests were not executed (read-only review).

Fix these issues in Kilo Cloud

Previous review (commit 1c2de7d)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 4
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/plugin/index.ts 251 configuredSkillPaths is a newly introduced camelCase compound; before (the name mark() already uses for the parameter) keeps the shared upstream line shorter.
packages/opencode/test/kilocode/fixtures/plugin-skill.ts 1 path import is now unused after switching to a template-string directory.
packages/opencode/test/kilocode/plugin-skill-path.test.ts 64 ${dir}-plugin-skills is a sibling of the tmpdir, so the scoped finalizer never removes it; each run leaves a skills tree in the system temp dir. An Effect.addFinalizer inside init would clean it up.
packages/opencode/test/kilocode/plugin-skill-path.test.ts 55 Assertion still only checks discovery; adding trusted: true pins the trust behavior this fix is about. (Content-substitution assertion covers it indirectly.)
Files Reviewed (5 files)
  • .changeset/trusted-plugin-skills.md - 0 issues
  • packages/opencode/src/kilocode/config/plugin-skill-origins.ts - 0 issues
  • packages/opencode/src/plugin/index.ts - 1 issue
  • packages/opencode/test/kilocode/fixtures/plugin-skill.ts - 1 issue
  • packages/opencode/test/kilocode/plugin-skill-path.test.ts - 2 issues

Resolved since the last review: the trust map is now derived from a pre-hook snapshot of cfg.skills.paths, so missing provenance stays fail-closed, and the fixture no longer writes to a fixed shared path in the system temp root. Fork hygiene is still good — the shared upstream file only gains a two-line annotated hook. Note the branch was rebased onto an older main (merge base 17cc5811); review is against that merge base. Tests were not executed (read-only review).

Fix these issues in Kilo Cloud

Previous review (commit 3613fbf)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/config/plugin-skill-origins.ts 13 Trust is inferred from a missing origin (fail-open). Correct for current code paths, but any future programmatic write to cfg.skills.paths silently becomes trusted. Snapshotting the paths before the config-hook loop and marking only the delta removes the assumption.

SUGGESTION

File Line Issue
packages/opencode/test/kilocode/plugin-skill-path.test.ts 63 Fixture skill is written to ${TMPDIR}/plugin-skills, a fixed path in the system temp root that is never cleaned up and is shared across runs/shards; a unique sibling dir keeps the out-of-project-root property.
packages/opencode/test/kilocode/plugin-skill-path.test.ts 55 Assertion only checks discovery; adding trusted: true pins the trust behavior the fix is about.
Files Reviewed (5 files)
  • .changeset/trusted-plugin-skills.md - 0 issues
  • packages/opencode/src/kilocode/config/plugin-skill-origins.ts - 1 issue
  • packages/opencode/src/plugin/index.ts - 0 issues
  • packages/opencode/test/kilocode/fixtures/plugin-skill.ts - 0 issues (see test file comment)
  • packages/opencode/test/kilocode/plugin-skill-path.test.ts - 2 issues

Notes: fork hygiene looks good — the shared upstream file only gains a two-line annotated hook and the logic lives under src/kilocode/. Plugin-declared trust is not an escalation over the existing model, since plugins from project config already execute arbitrary code without a trust gate. Tests were not executed (read-only review).

Fix these issues in Kilo Cloud


Reviewed by claude-opus-5 · Input: 40 · Output: 9.5K · Cached: 1.4M

Review guidance: REVIEW.md from base branch main

@noobezlol
noobezlol force-pushed the agent/fix-plugin-skill-origins branch from 3613fbf to 7bac10c Compare August 1, 2026 11:08
@@ -0,0 +1,11 @@
import path from "node:path"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SUGGESTION: Unused import

path is no longer used now that the directory is built with a template string, so this import is dead.

Suggested change
import path from "node:path"
export default async (input: { directory: string }) => ({

(Applying the suggestion above also drops the following blank line's purpose — if you prefer, just delete line 1 and the blank line manually.)


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To use Kilo from GitHub you first need to link your GitHub account to Kilo. Link your Kilo account to continue. After linking, mention me again in this issue or pull request.

init: (dir) =>
Effect.promise(async () => {
await Bun.write(
path.join(`${dir}-plugin-skills`, "example", "SKILL.md"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SUGGESTION: The sibling skills directory is never removed

Using ${dir}-plugin-skills fixes the shared-name problem from the earlier revision, but the scoped finalizer in tmpdirScoped (test/fixture/fixture.ts:157) only cleans dir itself, so each run leaves an opencode-test-<random>-plugin-skills tree behind in the system temp dir. init runs inside the tmpdir scope, so a yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(${dir}-plugin-skills, { recursive: true, force: true }))) inside the init effect (or wrapping the promise body in Effect.gen) would clean it up with the rest of the fixture.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To use Kilo from GitHub you first need to link your GitHub account to Kilo. Link your Kilo account to continue. After linking, mention me again in this issue or pull request.

Comment thread packages/opencode/src/plugin/index.ts Outdated
}

// Notify plugins of current config
const configuredSkillPaths = new Set(cfg.skills?.paths ?? []) // kilocode_change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SUGGESTION: Prefer a single-word name here

AGENTS.md asks for single-word locals unless a compound is genuinely needed; mark() already calls this parameter before, so the compound adds nothing and makes the shared upstream line longer than it needs to be.

Suggested change
const configuredSkillPaths = new Set(cfg.skills?.paths ?? []) // kilocode_change
const before = new Set(cfg.skills?.paths ?? []) // kilocode_change

The call on the line below would become PluginSkillOrigins.mark(cfg, before).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

To use Kilo from GitHub you first need to link your GitHub account to Kilo. Link your Kilo account to continue. After linking, mention me again in this issue or pull request.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

To stay organized pull requests are automatically closed after 30 days of inactivity. If the pull request is still relevant please reopen it or create a fresh new one.

@github-actions github-actions Bot closed this Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin skill loader intermittently fails with "Failed to parse skill" — only ~5/14 skills load

1 participant