agent-plugin-eval
Audit, score, and compare repositories containing portable Agent Plugins against the official Agent Plugins specification. Use when asked to review a plugin repo, check plugin.json or mcp.json conformance, assess bundled skills and MCP servers, produce an evidence-cited 0–100 plu
Install
npx skills add https://github.com/fabricioctelles/skills/tree/main/skills/agent-plugin-eval
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install fabricioctelles-skills@llmmart
git clone https://github.com/fabricioctelles/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole fabricioctelles/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Agent Plugin Evaluation
Treat the portable Agent Plugins specification as the authority. A client-native
manifest (e.g., .codex-plugin/plugin.json, .claude/settings.json,
.cursor/mcp.json) does not replace the required root plugin.json.
Parameters
| Parameter | Description | Default |
|---|---|---|
target |
Local repository/plugin path or Git URL | Ask if missing |
compare |
Optional second path or Git URL | None |
output |
Scorecard destination | Reply only; write only when requested |
spec_version |
Agent Plugins version to evaluate | Version declared by plugin.json, or 1.0.0 |
Safety boundary
Audit untrusted repositories statically. Do not run bundled executables, hooks,
install scripts, package managers, MCP servers, or networked tests unless the
user explicitly authorizes execution. Redact suspected secret values; report
only their location and kind. A secret-like key or value is a suspicion, not
confirmation: do not assign the FAIL gate without corroborating evidence such
as a recognized live credential format, a trusted secret scanner, repository
history/provenance, or user confirmation. Never test a credential against a
service merely to confirm it.
Workflow
- Resolve the plugin root. Use a local target in place. For a Git URL,
shallow-clone into a
mktemp -ddirectory. A plugin root contains rootplugin.json; if a repo has zero or multiple candidates, report the ambiguity instead of guessing. Done when every target maps to one explicit plugin root. - Load the governing rules. Read
references/spec-checklist.mdandreferences/rubric.md. For Agent Plugins1.0.0, use the bundled snapshot. For another declared version, or when the user asks for the latest spec, browse the canonical specification and schemas atagent-plugins.organd record the evaluated version and retrieval date. The normative text wins if it conflicts with JSON Schema. - Inventory every package path. Include dotfiles, symlinks, immediate skill children, extension namespaces, executable files, and files ignored by Git. Resolve every symlink and package-relative path against the plugin root. Done when every discovered path is accounted for as portable core, client extension, supporting file, or containment violation.
- Run the deterministic scan. Execute
python3 scripts/inspect_plugin.py <plugin-root> --json. Treat its output as evidence leads, not the final judgment. Confirm each reported issue in the source and addfile:lineor JSON-pointer evidence. Never weaken a normative finding merely because a client happens to accept it. - Review components completely. Inspect every immediate
skills/*/SKILL.mdand everymcpServersentry. Validate Agent Skills against their own specification. Assess instructions, resources, scripts, MCP configuration, extension isolation, cohesion, and practical utility. Ifskill-evaluationis available, it may deepen individual skill-quality analysis, but it never replaces this plugin-level rubric. - Classify conformance before scoring. Use the exact failure boundaries in
references/spec-checklist.md:PASS,PARTIAL, orFAIL. Keep client compatibility separate from portable conformance. A client-specific feature may be excellent for that client and still add zero portable coverage. - Score with cite-or-cut. Score all applicable rubric criteria from
references/rubric.md. Every score needs specific evidence; everyN/Aneeds a reason. Runscripts/score.pyfor the weighted result and gate cap; do not calculate it by hand. Done when all criteria and all findings are reconciled with the conformance status. - Answer in the requested language. Read
references/output-template.mdand emit that structure. Lead with the verdict, distinguish blockers from recommendations, and provide concrete fixes. Whencompareis set, evaluate both independently before computing deltas; never force the same N/A set on both plugins.
Gates and scoring
PASS: no normative violation found; no score cap.PARTIAL: non-fatal manifest deviation or invalid/skipped component; final score capped at 59.FAIL: fatal manifest/package-root failure, root-manifest escape, or confirmed embedded credential; final score capped at 39.- Keep the uncapped score visible so authors can distinguish design quality from release-blocking conformance.
Invoke the calculator with one criterion:score:weight triple per criterion:
python3 scripts/score.py --gate partial 1:90:3 2:80:3 3:NA:2
Evaluation with Jev (Optional)
When TypeSafe Jev is available, use it for subjective quality criteria. Jev provides calibrated probability judgments that augment the deterministic checks.
When to use Jev
| Evaluation Type | Use Jev? | Method |
|---|---|---|
| Axes 1-3 conformance | No | Deterministic (inspect_plugin.py) |
| Axis 4 product quality | Yes | Score (UX, docs, errors) |
| Axis 2 quality criteria | Yes | Score (schema design, naming) |
| Gate classification | Yes | Noul (pass/fail categories) |
| Secret detection | Yes | Noul (suspected/not_suspected) |
| Quality checklist | Yes | Noul (present/missing) |
Discovery protocol
from typesafe import jev_available
if jev_available():
from typesafe import Score, Noul
# Use Jev for subjective criteria
else:
# Fall back to heuristic scoring
Questions and integration
Questions are defined in scripts/jev_questions.json:
- 7 Score questions: Axis 4 (UX coherence, documentation clarity, error handling) and Axis 2 (validation, schema design, naming, API elegance)
- 20 Noul questions: Gates (G1-G4), secrets (4), quality checklist (7), component validity (4)
Score results (0.0-1.0) are averaged per axis and scaled to the rubric (0-25). Noul results provide categorical classifications for gates and checklists.
See references/jev-integration.md for full integration patterns and code
examples.
Output format
When Jev is used, the scorecard includes a jev section:
{
"jev": {
"available": true,
"quality_scores": { "ux_coherence": 0.72, ... },
"gate_classifications": { "G1": {"label": "conformant", "passed": true} },
"secret_findings": { "requires_review": false }
}
}
Gotchas
- The v1 portable core contains exactly Agent Skills and MCP servers. Hooks, commands, agents, apps, marketplaces, and distribution policy are client-specific unless placed in a valid extension namespace.
- Missing optional
skills/ormcp.jsonis not an error. A present path of the wrong filesystem kind is an invalid component type. - Unknown root manifest fields are schema violations but have the spec's narrow non-fatal handling; most other manifest schema violations reject the whole plugin.
- One invalid skill or MCP server must not be reported as if every independent component were invalid.
${PLUGIN_ROOT}and${PLUGIN_DATA}expand only in MCPargs,envvalues, andcwd; never incommand, URLs, or headers.- A high-quality client-native plugin can still fail the portable standard when
root
plugin.jsonis absent. Report both facts without averaging them away. - Keep possible credentials labeled “suspected” and redacted. A heuristic hit
alone lowers the security score and demands remediation review, but does not
become a confirmed-credential
FAILgate.
Final quality gate
- Every target resolved to exactly one root
- Every file, symlink, skill, MCP server, and extension inspected
- Every normative violation mapped to its correct failure boundary
- Every score cited and every N/A justified
- Suspected secrets redacted
- Score produced by
scripts/score.py - Comparison deltas use independently computed scores
Files (skills)
-
agents
-
default.yaml 294 B
interface: display_name: "Agent Plugin Eval" short_description: "Audit and compare portable agent plugins against the Agent Plugins specification" default_prompt: "Use $agent-plugin-eval to audit this plugin repository against Agent Plugins 1.0.0 and return an evidence-cited scorecard."
-
-
references
-
jev-integration.md 6.4 KB
# Jev Integration for Agent Plugin Eval ## Overview This skill integrates with [TypeSafe Jev](https://github.com/AugmentHCI/typesafe-jev) for subjective quality evaluation of agent plugins. Jev provides calibrated probability judgments (Score) and categorical classifications (Noul) that augment the deterministic checks in `inspect_plugin.py`. ## When to Use Jev | Evaluation Type | Use Jev? | Rationale | |-----------------|----------|-----------| | **Axes 1-3 Conformance** | No | Deterministic checks (JSON validity, schema presence, field existence) | | **Axis 4 Product Quality** | **Yes (Score)** | Subjective assessment of UX, documentation clarity, error handling | | **Axis 2 Quality Criteria** | **Yes (Score)** | Schema design elegance, naming consistency, API coherence | | **Gate Classification** | **Yes (Noul)** | Categorical pass/fail on structure, coherence, validity | | **Secret Detection** | **Yes (Noul)** | Pattern-based suspicion (confirmed requires external scanner) | | **Quality Checklist** | **Yes (Noul)** | Present/missing classification for files | ## Discovery Protocol Before invoking Jev, check availability: ```python from typesafe import jev_available if jev_available(): from typesafe import Score, Noul # Use Jev for subjective criteria else: # Fall back to heuristic scoring ``` ## Question Categories ### Score Questions (0.0-1.0 probability) Used for quality dimensions where gradations matter: | ID | Axis.Criterion | Evaluates | |----|----------------|-----------| | `ux_coherence` | 4.6 | Tool interaction coherence | | `documentation_clarity` | 4.8 | Docs completeness and clarity | | `error_handling_quality` | 4.9 | Error message quality | | `input_validation_robustness` | 2.15 | Validation thoroughness | | `schema_design_quality` | 2.16 | Schema design quality | | `naming_consistency` | 2.17 | Naming conventions | | `api_design_elegance` | 2.18 | API simplicity and elegance | ### Noul Questions (categorical labels) Used for binary or few-class decisions: | Category | Questions | Labels | |----------|-----------|--------| | **Gates (G1-G4)** | 4 | conformant/valid/coherent vs malformed/invalid/incoherent | | **Secrets** | 4 | suspected / not_suspected | | **Quality Checklist** | 7 | present / missing | | **Component Validity** | 4 | valid / invalid / missing | ## Integration Points ### 1. Score Integration (Axis 2 & 4) ```python def evaluate_quality_with_jev(context: dict) -> dict: """Evaluate subjective quality criteria using Jev Score.""" if not jev_available(): return heuristic_quality_score(context) from typesafe import Score scores = {} questions = load_jev_questions()["questions"]["score"] for q in questions: ctx = extract_context(context, q["context_required"]) result = Score(q["question"], context=ctx) scores[q["id"]] = { "probability": result.probability, "axis": q["axis"], "criterion": q["criterion"] } return aggregate_axis_scores(scores) ``` ### 2. Gate Classification (Noul) ```python def classify_gates_with_jev(context: dict) -> dict: """Classify conformance gates using Jev Noul.""" if not jev_available(): return deterministic_gate_check(context) from typesafe import Noul gates = {} questions = [q for q in load_jev_questions()["questions"]["noul"] if "gate" in q] for q in questions: ctx = extract_context(context, q["context_required"]) result = Noul(q["question"], labels=q["labels"], context=ctx) gates[q["gate"]] = { "label": result.label, "passed": result.label == q["labels"][0] # First label is positive } return gates ``` ### 3. Secret Detection (Noul) ```python def detect_secrets_with_jev(context: dict) -> dict: """Flag potential secrets for human review using Jev Noul.""" if not jev_available(): return regex_secret_scan(context) from typesafe import Noul findings = {} questions = [q for q in load_jev_questions()["questions"]["noul"] if q.get("category") == "secrets"] for q in questions: ctx = extract_context(context, q["context_required"]) result = Noul(q["question"], labels=q["labels"], context=ctx) findings[q["id"]] = { "suspected": result.label == "suspected", "confidence": result.probability } # Flag for human review if any suspected return { "findings": findings, "requires_review": any(f["suspected"] for f in findings.values()) } ``` ## Score Aggregation Jev Score returns probabilities [0.0, 1.0]. Map to rubric scores: ```python def aggregate_axis_scores(scores: dict, max_per_axis: int = 25) -> dict: """Aggregate Jev scores to rubric scale.""" axis_scores = {} for axis_num in [2, 4]: axis_items = [s for s in scores.values() if s["axis"] == axis_num] if axis_items: avg_prob = sum(s["probability"] for s in axis_items) / len(axis_items) axis_scores[f"axis_{axis_num}"] = round(avg_prob * max_per_axis, 1) return axis_scores ``` ## Fallback Behavior When Jev is unavailable, the skill falls back to: 1. **Quality scoring**: Heuristic checks (file existence, size, pattern matching) 2. **Gate classification**: Deterministic JSON/schema validation 3. **Secret detection**: Regex pattern matching (high false positive rate) The fallback is functional but less nuanced than Jev evaluation. ## Context Preparation Each question specifies required context. Extract from plugin repository: ```python CONTEXT_EXTRACTORS = { "plugin_manifest": lambda repo: read_file(repo, "plugin.json"), "tool_definitions": lambda repo: extract_tools(repo), "readme": lambda repo: read_file(repo, "README.md"), "skill_files": lambda repo: glob_read(repo, "**/SKILL.md"), "source_files_sample": lambda repo: sample_source(repo, max_files=10), "file_listing": lambda repo: list_files(repo), # ... etc } ``` ## Output Format Jev-enhanced evaluation adds a `jev` section to the report: ```json { "score": 78, "axes": { ... }, "gates": { ... }, "jev": { "available": true, "quality_scores": { "ux_coherence": 0.72, "documentation_clarity": 0.85, ... }, "gate_classifications": { "G1": {"label": "conformant", "passed": true}, ... }, "secret_findings": { "requires_review": false, "findings": { ... } } } } ``` -
output-template.md 3.1 KB
# Output Template Use the user's language. Preserve this section order, but keep empty prose brief. Omit the comparison section unless `compare` is supplied. ```markdown # Agent Plugin Evaluation — {plugin name} > Verdict: {PASS / PARTIAL / FAIL} — {one-sentence reason} > Evaluated: {date} > Source: {path or URL} ({commit when available}) > Standard: Agent Plugins {version} ({status}), retrieved {date} > Audit mode: static / authorized dynamic checks ## Summary | Metric | Value | |---|---| | Conformance | {PASS / PARTIAL / FAIL} | | Raw score | {n}/100 | | Final score | {n}/100 {cap explanation, if any} | | Grade | {A/B/C/D/F} | | Portable components | {skills count}, {MCP server count} | | Client extensions | {namespaces or none} | | Criteria scored / N/A | {n} / {n} | ## Release blockers 1. **{finding}** — `{file:line or JSON pointer}`; spec §{section}. Impact: {official failure boundary}. Fix: {smallest conforming change}. Write “None found” when the gate is PASS. Do not mix recommendations here. ## Scorecard | # | Criterion | Weight | Score | Evidence | |---|---|---:|---:|---| | 1 | Root package and manifest | 3x | {n}/100 | {citation} | | ... | ... | ... | ... | ... | | 18 | Maintenance and release hygiene | 1x | {n}/100 | {citation} | Use `N/A` plus a reason for conditional criteria. ## Findings by component ### Manifest and package {findings, including non-fatal deviations} ### Skills {one row or paragraph per discovered Skill; include conformance and quality} ### MCP servers {one row or paragraph per server; include transport and failure boundary} ### Client extensions {portable-vs-client-specific assessment} ### Security and containment {resolved paths, secret scan, and execution-risk findings; never reveal values; distinguish suspected from confirmed credentials and state the corroboration} ## Prioritized actions 1. **P0 — {action}.** Evidence: `{file:line}`. Verify by: {check}. 2. **P1 — {action}.** Evidence: `{file:line}`. Verify by: {check}. 3. **P2 — {action}.** Evidence: `{file:line}`. Verify by: {check}. ## Limits {Anything not executed or verified, unsupported extension specs, network limits, or ambiguity. State that static safety boundaries were intentional.} ``` ## Comparison mode Append after both independent evaluations: ```markdown ## Comparison — {A} vs {B} | Metric | {A} | {B} | Delta / winner | |---|---:|---:|---| | Conformance | {status} | {status} | {result} | | Raw score | {n} | {n} | {signed delta} | | Final score | {n} | {n} | {signed delta} | | Grade | {grade} | {grade} | {result} | | # | Criterion | {A} | {B} | Delta | |---|---|---:|---:|---:| | 1 | Root package and manifest | {n} | {n} | {signed delta} | | ... | ... | ... | ... | ... | | 18 | Maintenance and release hygiene | {n} | {n} | {signed delta} | ### Decision {Which plugin is more conformant, which has better uncapped design quality, and which is safer to release. Do not collapse those into one vague winner.} ### Shared and unique actions - **Both:** {shared action} - **{A}:** {specific action} - **{B}:** {specific action} ``` -
rubric.md 4.9 KB
# Agent Plugin Evaluation Rubric Score each applicable criterion from 0–100 and multiply by its weight. Mark a conditional criterion `N/A` only when the relevant component is absent; exclude N/A from numerator and denominator. Cite concrete `file:line`, JSON pointer, or resolved-path evidence for every score. ## Scale | Score | Meaning | |---|---| | 0 | Missing or wholly broken | | 1–25 | Token attempt; major requirements unmet | | 26–50 | Partial implementation with serious gaps | | 51–75 | Functional, with meaningful improvements needed | | 76–90 | Strong; minor gaps | | 91–100 | Exemplary and release-ready | ## Axis 1 — Portable conformance | # | Criterion | Weight | Key question | |---|---|---:|---| | 1 | Root package and manifest | 3x | Is there exactly one regular root `plugin.json`, with all resolved package paths contained? | | 2 | Schema, required fields, and name | 3x | Does the manifest target a supported canonical schema and satisfy every fatal required-field/name rule? | | 3 | Closed metadata model | 2x | Are top-level and author fields closed, optional types correct, and metadata internally accurate? | | 4 | Fixed discovery and version coherence | 2x | Are standard components only in fixed locations, with matching spec versions and correct filesystem kinds? | ## Axis 2 — Components and integration | # | Criterion | Weight | Applies when | Key question | |---|---|---:|---|---| | 5 | Agent Skills conformance | 2x | At least one Skill is discovered | Does every immediate Skill satisfy the Agent Skills format and containment rules? | | 6 | Agent Skills quality | 1x | At least one Skill is discovered | Are triggers, instructions, resources, validation, and progressive disclosure useful and precise? Aggregate all Skills; identify outliers. | | 7 | MCP configuration conformance | 2x | `mcp.json` exists | Are the closed top level and every server variant valid under the matching schema? | | 8 | MCP runtime portability | 2x | At least one stdio server exists | Are command, args, cwd, env, placeholders, bundled executables, and dependencies deterministic across installations? | | 9 | Remote MCP transport quality | 2x | At least one remote server exists | Are URL, TLS, headers, transport choice, redirect assumptions, and authentication boundaries safe and portable? | | 10 | Extension isolation | 1x | Client-specific data/files exist | Are extensions correctly reverse-domain namespaced without masquerading as portable core? | ## Axis 3 — Safety and resilience | # | Criterion | Weight | Key question | |---|---|---:|---| | 11 | Path containment | 3x | Do symlinks, commands, working directories, and package-relative references remain within their required roots after resolution? | | 12 | Secrets and least exposure | 3x | Are visible manifests/configs free of credentials and unnecessarily sensitive literals? Are suspected values redacted in the report? | | 13 | Component independence | 2x | Can valid Skills and MCP entries remain useful when an unrelated component is absent, invalid, unsupported, or unavailable? | | 14 | Static audit safety | 1x | Can reviewers and clients inspect/install the package without executing hidden setup, hooks, or side effects? | ## Axis 4 — Product quality | # | Criterion | Weight | Key question | |---|---|---:|---| | 15 | Cohesion and portable value | 2x | Do the components form one understandable plugin and deliver meaningful value on conformant clients? | | 16 | Discoverability and documentation | 1x | Do description, version, repository, license, keywords, and user guidance make purpose, trust, setup, and limits clear? | | 17 | Validation evidence | 2x | Are schemas, Skills, scripts, and MCP behavior covered by safe reproducible checks or fixtures? Do tests exercise failure boundaries? | | 18 | Maintenance and release hygiene | 1x | Are versions, dependencies, generated artifacts, executable bits, ignored files, and client extensions intentional and maintainable? | ## Weighted result and gates Compute the uncapped weighted score: ```text raw = sum(score × weight) / sum(applicable weights) ``` Then apply the conformance gate from `spec-checklist.md`: | Gate | Cap | Meaning | |---|---:|---| | PASS | none | No normative violation found | | PARTIAL | 59 | At least one non-fatal deviation or invalid/skipped component | | FAIL | 39 | Plugin-level fatal failure, root-manifest escape, or confirmed credential | Grades use the final capped score: A 80–100, B 60–79, C 40–59, D 20–39, F 0–19. Always report both raw and final scores when a cap applies. ## Comparison rules - Score each plugin independently, including its N/A denominator and gate. - Use final score for the headline delta; show raw delta separately when either plugin is capped. - Compare conformance statuses directly; do not imply that a numerical lead can compensate for a release blocker. - Distinguish shared weaknesses from plugin-specific regressions and end with a per-plugin action list. -
spec-checklist.md 6.1 KB
# Agent Plugins 1.0.0 Conformance Checklist Use this snapshot for Agent Plugins `1.0.0`. The canonical normative text at <https://agent-plugins.org/specification> governs; JSON Schemas are supporting validation artifacts. Version 1.0.0 was published as a Working Draft, so verify the canonical source when evaluating another version or a request for “latest.” ## 1. Package and manifest - Require a regular `plugin.json` at the plugin root. Its resolved path and all package-supplied paths must remain inside the resolved plugin root. - Require a JSON object with canonical `$schema` and `name`. - For 1.0.0, require `https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`. - Permit only `$schema`, `name`, `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords`, and `extensions` at the top level. - Constrain `name` to 1–64 lowercase `a-z`, digits, hyphens, and periods; start and end alphanumeric; reject `--` and `..`. - Validate optional field types. `author` may contain only string-valued `name`, `email`, and `url`. Semantic Versioning and SPDX are recommended, not required by type validation. - Treat unknown top-level fields as reported-and-ignored schema violations, not fatal loader errors. Treat a non-object `extensions` similarly. Other manifest schema violations reject the plugin. ## 2. Discovery and skills - Discover Skills only from immediate child directories of `skills/` that contain a regular file named exactly `SKILL.md`; do not recurse deeper. - Validate every discovered skill against <https://agentskills.io/specification>. Skip an invalid skill without disabling valid siblings or MCP. - Treat an absent `skills/` as valid. If present but not a directory, mark the Skills component type invalid while continuing with other types. - Do not treat client-native folders or manifest entries as portable component discovery. Agent Plugins v1 defines exactly Skills and MCP servers. ## 3. MCP configuration - Discover MCP only from regular root `mcp.json`. If absent, MCP is simply not provided. If present with the wrong filesystem kind, MCP is invalid. - Require a JSON object containing only `$schema` and `mcpServers`, both required. For 1.0.0 use `https://agent-plugins.org/schemas/1.0.0/mcp.schema.json`. - Require the MCP schema version to match `plugin.json`. A top-level MCP error disables MCP but not Skills. - Validate each server independently as exactly one closed variant: - `stdio`: required string `command`; optional string-array `args`, string-map `env`, and string `cwd`. - `streamable-http` or deprecated `sse`: required absolute `url`; optional string-map `headers`. - For `stdio`, keep `command` one executable token: a bare executable or a plugin-relative path beginning `./`. Do not expand placeholders in it. - Allow `cwd` only as `./...`, `${PLUGIN_ROOT}[/...]`, or `${PLUGIN_DATA}[/...]`, with post-resolution containment. - Expand exactly `${PLUGIN_ROOT}` and `${PLUGIN_DATA}`, once and non-recursively, only in `args`, `env` values, and `cwd`. Preserve unknown placeholder-like text literally. Forbid reserved names in configured `env`. - For remote transports, require absolute HTTP(S), no userinfo or fragment, and HTTPS except exact localhost or loopback IPs. Treat header names case-insensitively and reject duplicates by casing. - Never embed credentials in `env` or `headers`. Agent Plugins v1 has no portable OAuth or credential-reference fields. ## 4. Client extensions - Put client-specific manifest data under `extensions` keys using stable reverse-domain namespaces whose values are objects. - Put client-specific files in root directories named exactly after their namespaces. Either manifest data or a directory may exist independently. - Do not assign portable semantics to extension contents. Evaluate an extension against its owning client's specification only when the user asks for that compatibility profile. ## 5. Failure boundaries and evaluation gate | Finding | Official loading boundary | Evaluation gate | |---|---|---| | Missing/unreadable root manifest, unsupported schema, invalid required field, or fatal manifest schema error | Reject plugin; load no components | `FAIL` | | Package or configured path resolves outside allowed root/data boundary | Reject/skip at the narrowest normative boundary; deny access | `FAIL` when the root manifest escapes; otherwise `PARTIAL` at the affected boundary | | Unknown root manifest field or non-object `extensions` | Report and ignore field; continue | `PARTIAL` | | `skills/` or `mcp.json` present with wrong filesystem kind | Disable that component type; continue | `PARTIAL` | | Invalid individual Skill | Skip that Skill; continue | `PARTIAL` | | Invalid MCP top level or version mismatch | Disable MCP; continue | `PARTIAL` | | Invalid/unsupported/failing MCP server entry | Skip that server; continue | `PARTIAL` | | Missing optional component location | No error | No downgrade | | Confirmed embedded credential in visible package config | Unsafe to release | `FAIL` | A secret-like field name or literal is not confirmation by itself. Keep a heuristic match as a redacted suspicion until corroborated by a recognized live credential format, trusted scanner, repository provenance, or the user. Do not authenticate with a suspected value as a validation technique. Do not promote `PARTIAL` to `FAIL` merely because one component fails. Do not downgrade a normative MUST violation to a recommendation because a specific client is permissive. ## 6. Primary sources - [Agent Plugins overview](https://agent-plugins.org/) - [Agent Plugins Specification 1.0.0](https://agent-plugins.org/specification) - [Plugin JSON Schema 1.0.0](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json) - [MCP JSON Schema 1.0.0](https://agent-plugins.org/schemas/1.0.0/mcp.schema.json) - [Vercel announcement](https://vercel.com/blog/introducing-agent-plugins) The Vercel announcement is useful context for the portability goal but is not normative. Client-specific creator tooling (e.g., OpenAI plugin-creator, Anthropic Claude Desktop, Cursor MCP integration) is a compatibility reference, not a replacement for the portable specification.
-
-
scripts
-
inspect_plugin.py 19.3 KB
#!/usr/bin/env python3 """Static Agent Plugins 1.0.0 inspector; never executes plugin code. The scanner finds one plugin root in a repository, checks high-confidence manifest/MCP/path rules, and emits evidence leads. Human review remains required for Agent Skill quality, client extensions, and contextual findings. """ from __future__ import annotations import argparse import ipaddress import json import os from pathlib import Path import re import sys from typing import Any from urllib.parse import urlsplit PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json" PLUGIN_FIELDS = { "$schema", "name", "version", "description", "author", "homepage", "repository", "license", "keywords", "extensions", } NAME_RE = re.compile(r"^(?=.{1,64}$)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$") NAMESPACE_RE = re.compile(r"^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+$") SKILL_NAME_RE = re.compile(r"^(?=.{1,64}$)[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$") SECRET_KEY_RE = re.compile( r"(?:authorization|api[-_]?key|access[-_]?token|(?:^|[-_])token(?:$|[-_])|secret|password|passwd)", re.I ) HEADER_NAME_RE = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$") class Report: def __init__(self, target: Path) -> None: self.target = str(target) self.root: Path | None = None self.findings: list[dict[str, str]] = [] self.skills = 0 self.mcp_servers = 0 self.extensions: list[str] = [] def add(self, severity: str, code: str, path: str, message: str, spec: str) -> None: self.findings.append({ "severity": severity, "code": code, "path": path, "message": message, "spec": spec, }) @property def gate(self) -> str: severities = {item["severity"] for item in self.findings} if "fatal" in severities or "security" in severities: return "FAIL" if "partial" in severities: return "PARTIAL" return "PASS" def payload(self) -> dict[str, Any]: counts: dict[str, int] = {} for finding in self.findings: key = finding["severity"] counts[key] = counts.get(key, 0) + 1 return { "target": self.target, "plugin_root": str(self.root) if self.root else None, "spec_version": "1.0.0", "gate": self.gate, "counts": counts, "inventory": { "skills": self.skills, "mcp_servers": self.mcp_servers, "extension_namespaces": self.extensions, }, "findings": self.findings, "notice": "Static evidence leads only; confirm in source before scoring.", } def relative(path: Path, root: Path) -> str: try: return str(path.relative_to(root)) or "." except ValueError: return str(path) def plugin_candidates(target: Path) -> list[Path]: if (target / "plugin.json").exists() or (target / "plugin.json").is_symlink(): return [target] found: list[Path] = [] for base, dirs, files in os.walk(target, followlinks=False): dirs[:] = [d for d in dirs if d not in {".git", "node_modules", ".venv", "venv"}] base_path = Path(base) if "plugin.json" in files or (base_path / "plugin.json").is_symlink(): found.append(base_path) return sorted(set(found)) def load_json(path: Path, report: Report, severity: str, code: str, spec: str) -> Any | None: try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError) as exc: report.add(severity, code, str(path), f"Cannot parse JSON: {exc}", spec) return None def contained(path: Path, root: Path) -> bool: try: path.resolve(strict=False).relative_to(root.resolve(strict=True)) return True except (OSError, ValueError): return False def check_symlinks(root: Path, report: Report) -> None: for base, dirs, files in os.walk(root, followlinks=False): for name in [*dirs, *files]: path = Path(base) / name if path.is_symlink() and not contained(path, root): report.add( "partial", "PATH_ESCAPE", relative(path, root), "Symlink resolves outside the plugin root.", "§4.1", ) def valid_name(name: Any) -> bool: return ( isinstance(name, str) and bool(NAME_RE.fullmatch(name)) and "--" not in name and ".." not in name ) def check_manifest(root: Path, report: Report) -> dict[str, Any] | None: path = root / "plugin.json" if not path.is_file() or not contained(path, root): report.add("fatal", "MANIFEST_MISSING", "plugin.json", "Root plugin.json is not a contained regular file.", "§4.1, §5.1") return None data = load_json(path, report, "fatal", "MANIFEST_JSON", "§5.2") if data is None: return None if not isinstance(data, dict): report.add("fatal", "MANIFEST_OBJECT", "plugin.json", "Manifest top level must be an object.", "§5.2") return None if data.get("$schema") != PLUGIN_SCHEMA: report.add("fatal", "PLUGIN_SCHEMA", "plugin.json#/$schema", f"Expected canonical Agent Plugins 1.0.0 schema {PLUGIN_SCHEMA}.", "§5.2") if not valid_name(data.get("name")): report.add("fatal", "PLUGIN_NAME", "plugin.json#/name", "Name violates the 1-64 character lowercase naming constraints.", "§5.3, §5.5") for key in sorted(set(data) - PLUGIN_FIELDS): report.add("partial", "UNKNOWN_MANIFEST_FIELD", f"plugin.json#/{key}", "Unknown top-level field is non-conforming and must be reported and ignored.", "§5.2") string_fields = {"version", "description", "homepage", "repository", "license"} for key in sorted(string_fields & set(data)): if not isinstance(data[key], str): report.add("fatal", "METADATA_TYPE", f"plugin.json#/{key}", "Metadata field must be a string.", "§5.4") if "keywords" in data and not ( isinstance(data["keywords"], list) and all(isinstance(item, str) for item in data["keywords"]) ): report.add("fatal", "KEYWORDS_TYPE", "plugin.json#/keywords", "keywords must be an array of strings.", "§5.4") if "author" in data: author = data["author"] if not isinstance(author, dict): report.add("fatal", "AUTHOR_TYPE", "plugin.json#/author", "author must be an object.", "§5.4") else: extra = set(author) - {"name", "email", "url"} if extra or any(not isinstance(value, str) for value in author.values()): report.add("fatal", "AUTHOR_FIELDS", "plugin.json#/author", "author may contain only string name, email, and url fields.", "§5.4") if "extensions" in data: extensions = data["extensions"] if not isinstance(extensions, dict): report.add("partial", "EXTENSIONS_OBJECT", "plugin.json#/extensions", "Non-object extensions is reported and ignored.", "§8.1") else: for namespace, value in extensions.items(): report.extensions.append(namespace) if not NAMESPACE_RE.fullmatch(namespace): report.add("partial", "EXTENSION_NAMESPACE", f"plugin.json#/extensions/{namespace}", "Extension key is not a clear reverse-domain namespace.", "§8") if not isinstance(value, dict): report.add("partial", "EXTENSION_VALUE", f"plugin.json#/extensions/{namespace}", "Extension namespace value must be an object for a conforming package.", "§8.1") return data def parse_frontmatter(path: Path) -> dict[str, str] | None: try: lines = path.read_text(encoding="utf-8").splitlines() except (OSError, UnicodeError): return None if not lines or lines[0].strip() != "---": return None try: end = next(i for i, line in enumerate(lines[1:], 1) if line.strip() == "---") except StopIteration: return None result: dict[str, str] = {} body = lines[1:end] i = 0 while i < len(body): line = body[i] match = re.match(r"^([A-Za-z0-9_-]+):\s*(.*?)\s*$", line) if match: key, raw = match.groups() if raw in {">", "|", ">-", "|-", ">+", "|+"}: parts: list[str] = [] i += 1 while i < len(body) and (not body[i].strip() or body[i][0].isspace()): parts.append(body[i].strip()) i += 1 result[key] = "\n".join(parts).strip() continue result[key] = raw.strip("'\"") i += 1 return result def check_skills(root: Path, report: Report) -> None: skills = root / "skills" if not skills.exists() and not skills.is_symlink(): return if not skills.is_dir() or not contained(skills, root): report.add("partial", "SKILLS_KIND", "skills", "Present skills path is not a contained directory.", "§6.2") return for child in sorted(skills.iterdir()): skill_file = child / "SKILL.md" if not child.is_dir() or not skill_file.is_file(): continue report.skills += 1 if not contained(skill_file, root): report.add("partial", "SKILL_ESCAPE", relative(skill_file, root), "Discovered SKILL.md resolves outside the plugin root.", "§4.1, §7.1") continue frontmatter = parse_frontmatter(skill_file) if frontmatter is None: report.add("partial", "SKILL_FRONTMATTER", relative(skill_file, root), "SKILL.md lacks parseable YAML frontmatter; run an Agent Skills validator.", "§7.1") continue name = frontmatter.get("name", "") description = frontmatter.get("description", "") if not name or not description: report.add("partial", "SKILL_REQUIRED", relative(skill_file, root), "Skill frontmatter is missing name or description.", "§7.1") if name and (not SKILL_NAME_RE.fullmatch(name) or "--" in name): report.add("partial", "SKILL_NAME_FORMAT", relative(skill_file, root), "Skill name violates Agent Skills naming constraints.", "§7.1 / Agent Skills") if len(description) > 1024: report.add("partial", "SKILL_DESCRIPTION", relative(skill_file, root), "Skill description exceeds 1024 characters.", "§7.1 / Agent Skills") if name and name != child.name: report.add("partial", "SKILL_NAME", relative(skill_file, root), "Skill frontmatter name does not match its immediate directory.", "§7.1") def looks_like_secret(key: str, value: str) -> bool: if not SECRET_KEY_RE.search(key) or not value.strip(): return False return "${" not in value and not re.search(r"(?:example|placeholder|replace|dummy|public)", value, re.I) def check_stdio(name: str, server: dict[str, Any], root: Path, report: Report) -> None: pointer = f"mcp.json#/mcpServers/{name}" allowed = {"type", "command", "args", "env", "cwd"} if set(server) - allowed: report.add("partial", "MCP_STDIO_FIELDS", pointer, "stdio entry contains fields outside its closed variant.", "§7.2.1") command = server.get("command") if not isinstance(command, str) or not command: report.add("partial", "MCP_COMMAND", pointer + "/command", "stdio command must be a non-empty executable token.", "§7.2.1") elif "${" in command or (not command.startswith("./") and any(ch.isspace() for ch in command)) or command.startswith("../"): report.add("partial", "MCP_COMMAND_TOKEN", pointer + "/command", "command must be one bare or ./ plugin-relative token without placeholder expansion.", "§7.2.1") elif command.startswith("./") and not contained(root / command[2:], root): report.add("partial", "MCP_COMMAND_ESCAPE", pointer + "/command", "Plugin-relative command resolves outside the plugin root.", "§4.1, §7.2.1") args = server.get("args") if args is not None and not (isinstance(args, list) and all(isinstance(v, str) for v in args)): report.add("partial", "MCP_ARGS", pointer + "/args", "args must be an array of strings.", "§7.2.1") env = server.get("env") if env is not None: if not isinstance(env, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in env.items()): report.add("partial", "MCP_ENV", pointer + "/env", "env must be an object of string values.", "§7.2.1") else: for key, value in env.items(): if key in {"PLUGIN_ROOT", "PLUGIN_DATA"}: report.add("partial", "MCP_RESERVED_ENV", pointer + f"/env/{key}", "Reserved plugin environment variables are client-supplied.", "§9.2") if looks_like_secret(key, value): report.add("warning", "POSSIBLE_SECRET", pointer + f"/env/{key}", "Possible embedded credential in visible env config (value redacted); confirm before assigning FAIL.", "§9.2") cwd = server.get("cwd") if cwd is not None: valid_form = isinstance(cwd, str) and ( cwd.startswith("./") or cwd == "${PLUGIN_ROOT}" or cwd.startswith("${PLUGIN_ROOT}/") or cwd == "${PLUGIN_DATA}" or cwd.startswith("${PLUGIN_DATA}/") ) if not valid_form: report.add("partial", "MCP_CWD", pointer + "/cwd", "cwd is not a permitted plugin/data-rooted form.", "§7.2.1") elif cwd.startswith("./") and not contained(root / cwd[2:], root): report.add("partial", "MCP_CWD_ESCAPE", pointer + "/cwd", "cwd resolves outside the plugin root.", "§4.1, §7.2.1") def is_loopback(host: str | None) -> bool: if host == "localhost": return True if not host: return False try: return ipaddress.ip_address(host).is_loopback except ValueError: return False def check_remote(name: str, server: dict[str, Any], report: Report) -> None: pointer = f"mcp.json#/mcpServers/{name}" if set(server) - {"type", "url", "headers"}: report.add("partial", "MCP_REMOTE_FIELDS", pointer, "Remote entry contains fields outside its closed variant.", "§7.2.1") raw_url = server.get("url") valid = isinstance(raw_url, str) try: parsed = urlsplit(raw_url) if valid else None hostname = parsed.hostname if parsed else None userinfo = bool(parsed and (parsed.username or parsed.password)) except ValueError: parsed = None hostname = None userinfo = False if not valid or parsed is None or parsed.scheme not in {"http", "https"} or not hostname or userinfo or parsed.fragment: report.add("partial", "MCP_URL", pointer + "/url", "Remote URL must be absolute HTTP(S), without userinfo or fragment.", "§7.2.1") elif parsed.scheme == "http" and not is_loopback(hostname): report.add("partial", "MCP_TLS", pointer + "/url", "Non-loopback MCP endpoint must use HTTPS.", "§7.2.1") headers = server.get("headers") if headers is not None: if not isinstance(headers, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in headers.items()): report.add("partial", "MCP_HEADERS", pointer + "/headers", "headers must be an object of strings.", "§7.2.1") else: lowered: set[str] = set() for key, value in headers.items(): if not HEADER_NAME_RE.fullmatch(key) or "\r" in value or "\n" in value: report.add("partial", "MCP_HEADER_SYNTAX", pointer + f"/headers/{key}", "Header name or value is not a valid HTTP field.", "§7.2.1") if key.lower() in lowered: report.add("partial", "MCP_HEADER_DUPLICATE", pointer + "/headers", "Header names duplicate case-insensitively.", "§7.2.1") lowered.add(key.lower()) if looks_like_secret(key, value): report.add("warning", "POSSIBLE_SECRET", pointer + f"/headers/{key}", "Possible embedded credential in visible headers (value redacted); confirm before assigning FAIL.", "§7.2.1") def check_mcp(root: Path, report: Report) -> None: path = root / "mcp.json" if not path.exists() and not path.is_symlink(): return if not path.is_file() or not contained(path, root): report.add("partial", "MCP_KIND", "mcp.json", "Present mcp.json is not a contained regular file.", "§6.2") return data = load_json(path, report, "partial", "MCP_JSON", "§7.2.2") if data is None: return if not isinstance(data, dict): report.add("partial", "MCP_OBJECT", "mcp.json", "MCP top level must be an object.", "§7.2.1") return if set(data) != {"$schema", "mcpServers"}: report.add("partial", "MCP_TOP_LEVEL", "mcp.json", "MCP top level must contain exactly $schema and mcpServers.", "§7.2.1") if data.get("$schema") != MCP_SCHEMA: report.add("partial", "MCP_SCHEMA", "mcp.json#/$schema", f"Expected canonical matching schema {MCP_SCHEMA}.", "§7.2.1, §10.1") servers = data.get("mcpServers") if not isinstance(servers, dict): report.add("partial", "MCP_SERVERS", "mcp.json#/mcpServers", "mcpServers must be an object.", "§7.2.1") return report.mcp_servers = len(servers) for name, server in servers.items(): pointer = f"mcp.json#/mcpServers/{name}" if not isinstance(server, dict): report.add("partial", "MCP_SERVER_OBJECT", pointer, "Server entry must be an object.", "§7.2.1") continue server_type = server.get("type") if server_type == "stdio": check_stdio(name, server, root, report) elif server_type in {"streamable-http", "sse"}: check_remote(name, server, report) else: report.add("partial", "MCP_SERVER_TYPE", pointer + "/type", "Unknown or missing MCP transport type.", "§7.2.1") def inspect(target: Path) -> Report: report = Report(target) if not target.is_dir(): report.add("fatal", "TARGET", str(target), "Target is not a directory.", "Package model") return report candidates = plugin_candidates(target) if len(candidates) != 1: report.add("fatal", "PLUGIN_ROOT_AMBIGUOUS", str(target), f"Expected exactly one plugin root; found {len(candidates)}.", "§4, §5.1") return report root = candidates[0].resolve(strict=True) report.root = root check_symlinks(root, report) check_manifest(root, report) check_skills(root, report) check_mcp(root, report) return report def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("target", type=Path) parser.add_argument("--json", action="store_true", dest="as_json") args = parser.parse_args() report = inspect(args.target.expanduser().resolve(strict=False)) payload = report.payload() if args.as_json: print(json.dumps(payload, indent=2, ensure_ascii=False)) else: print(f"gate: {payload['gate']}") print(f"plugin root: {payload['plugin_root'] or 'unresolved'}") print(f"skills: {payload['inventory']['skills']}") print(f"MCP servers: {payload['inventory']['mcp_servers']}") for item in payload["findings"]: print(f"[{item['severity'].upper()}] {item['code']} {item['path']}: {item['message']} ({item['spec']})") return {"PASS": 0, "PARTIAL": 1, "FAIL": 2}[report.gate] if __name__ == "__main__": sys.exit(main()) -
jev_questions.json 7.6 KB
{ "version": "1.0", "skill": "agent-plugin-eval", "description": "Jev questions for subjective plugin quality evaluation", "questions": { "score": [ { "id": "ux_coherence", "axis": 4, "criterion": 6, "question": "Does the plugin provide a coherent, intuitive experience when tools are used together?", "context_required": ["plugin_manifest", "tool_definitions", "sample_interactions"] }, { "id": "documentation_clarity", "axis": 4, "criterion": 8, "question": "Is the documentation clear, complete, and useful for both users and integrators?", "context_required": ["readme", "skill_instructions", "steering_files"] }, { "id": "error_handling_quality", "axis": 4, "criterion": 9, "question": "Are errors handled gracefully with clear, actionable messages?", "context_required": ["tool_definitions", "error_examples", "validation_logic"] }, { "id": "input_validation_robustness", "axis": 2, "criterion": 15, "question": "Is input validation thorough without being overly restrictive?", "context_required": ["schema_definitions", "validation_rules"] }, { "id": "schema_design_quality", "axis": 2, "criterion": 16, "question": "Are the JSON schemas well-designed with appropriate constraints and descriptions?", "context_required": ["tool_schemas", "type_definitions"] }, { "id": "naming_consistency", "axis": 2, "criterion": 17, "question": "Are naming conventions consistent and semantically meaningful across tools?", "context_required": ["tool_names", "parameter_names", "resource_names"] }, { "id": "api_design_elegance", "axis": 2, "criterion": 18, "question": "Is the API design elegant, avoiding unnecessary complexity?", "context_required": ["tool_definitions", "workflow_patterns"] } ], "noul": [ { "id": "gate_manifest_structure", "gate": "G1", "question": "Does the manifest follow valid JSON structure with required fields (name, version)?", "labels": ["conformant", "malformed"], "context_required": ["plugin_manifest_raw"] }, { "id": "gate_schema_validity", "gate": "G2", "question": "Are all tool schemas valid JSON Schema with proper type definitions?", "labels": ["valid", "invalid"], "context_required": ["tool_schemas_raw"] }, { "id": "gate_skill_coherence", "gate": "G3", "question": "Do bundled skills have coherent instructions that match tool capabilities?", "labels": ["coherent", "incoherent"], "context_required": ["skill_files", "tool_definitions"] }, { "id": "gate_transport_config", "gate": "G4", "question": "Is the transport configuration (stdio/http) properly specified and consistent?", "labels": ["valid", "misconfigured"], "context_required": ["transport_config", "server_entry"] }, { "id": "secret_env_vars", "category": "secrets", "question": "Do environment variables appear to contain hardcoded secrets rather than references?", "labels": ["suspected", "not_suspected"], "context_required": ["env_definitions", "config_files"] }, { "id": "secret_in_code", "category": "secrets", "question": "Does the source code contain patterns suggesting embedded credentials?", "labels": ["suspected", "not_suspected"], "context_required": ["source_files_sample"] }, { "id": "secret_in_docs", "category": "secrets", "question": "Do documentation or example files contain what appear to be real credentials?", "labels": ["suspected", "not_suspected"], "context_required": ["readme", "examples"] }, { "id": "secret_exposed_endpoint", "category": "secrets", "question": "Are there hardcoded URLs or endpoints that might expose internal systems?", "labels": ["suspected", "not_suspected"], "context_required": ["source_files_sample", "config_files"] }, { "id": "checklist_readme_exists", "category": "quality_checklist", "question": "Is there a README with installation and usage instructions?", "labels": ["present", "missing"], "context_required": ["file_listing"] }, { "id": "checklist_license", "category": "quality_checklist", "question": "Is there a LICENSE file with a recognized open source license?", "labels": ["present", "missing_or_unclear"], "context_required": ["file_listing", "license_content"] }, { "id": "checklist_changelog", "category": "quality_checklist", "question": "Is there a CHANGELOG or version history?", "labels": ["present", "missing"], "context_required": ["file_listing"] }, { "id": "checklist_tests", "category": "quality_checklist", "question": "Are there automated tests for the plugin functionality?", "labels": ["present", "missing"], "context_required": ["file_listing", "test_files"] }, { "id": "checklist_ci", "category": "quality_checklist", "question": "Is there CI/CD configuration for automated testing?", "labels": ["present", "missing"], "context_required": ["file_listing", "ci_config"] }, { "id": "checklist_examples", "category": "quality_checklist", "question": "Are there usage examples or sample interactions?", "labels": ["present", "missing"], "context_required": ["file_listing", "examples_dir"] }, { "id": "checklist_security_policy", "category": "quality_checklist", "question": "Is there a SECURITY.md or security policy?", "labels": ["present", "missing"], "context_required": ["file_listing"] }, { "id": "component_plugin_json", "category": "component_validity", "question": "Is plugin.json present and parseable?", "labels": ["valid", "invalid", "missing"], "context_required": ["plugin_json_raw"] }, { "id": "component_mcp_json", "category": "component_validity", "question": "Is mcp.json present and properly structured?", "labels": ["valid", "invalid", "missing"], "context_required": ["mcp_json_raw"] }, { "id": "component_skill_md", "category": "component_validity", "question": "Are SKILL.md files present with proper frontmatter?", "labels": ["valid", "invalid", "missing"], "context_required": ["skill_files_raw"] }, { "id": "component_steering", "category": "component_validity", "question": "Are steering files properly formatted with valid YAML frontmatter?", "labels": ["valid", "invalid", "not_applicable"], "context_required": ["steering_files_raw"] } ] }, "usage": { "score_aggregation": "For Axis 2 and 4 quality scores, average Jev Score responses (0.0-1.0) and scale to rubric (0-25 per axis)", "gate_evaluation": "Gates pass if Jev Noul returns positive label (conformant/valid/coherent)", "secret_detection": "Flag for human review if any secret question returns suspected", "checklist_scoring": "Count present labels, report as X/7 completeness" } } -
score.py 3 KB
#!/usr/bin/env python3 """Compute an Agent Plugin evaluation score with a conformance gate. Usage: score.py [--gate pass|partial|fail] 1:80:3 2:65:3 ... 8:NA:2 Each criterion is ID:SCORE:WEIGHT. NA or N/A excludes a criterion from both sums. PARTIAL caps the final score at 59; FAIL caps it at 39. """ from __future__ import annotations import argparse import sys CAPS = {"pass": None, "partial": 59.0, "fail": 39.0} def grade(score: float) -> str: if score >= 80: return "A" if score >= 60: return "B" if score >= 40: return "C" if score >= 20: return "D" return "F" def parse_triple(value: str) -> tuple[str, float | None, float]: try: criterion, score_text, weight_text = value.split(":") except ValueError as exc: raise argparse.ArgumentTypeError( f"{value!r}: expected criterion:score:weight" ) from exc if not criterion: raise argparse.ArgumentTypeError("criterion ID cannot be empty") try: weight = float(weight_text) except ValueError as exc: raise argparse.ArgumentTypeError(f"{value!r}: weight must be numeric") from exc if weight <= 0: raise argparse.ArgumentTypeError(f"{value!r}: weight must be positive") if score_text.strip().upper() in {"NA", "N/A"}: return criterion, None, weight try: score = float(score_text) except ValueError as exc: raise argparse.ArgumentTypeError(f"{value!r}: score must be 0-100 or NA") from exc if not 0 <= score <= 100: raise argparse.ArgumentTypeError(f"{value!r}: score outside 0-100") return criterion, score, weight def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--gate", choices=CAPS, default="pass") parser.add_argument("criteria", nargs="+", type=parse_triple) args = parser.parse_args() numerator = 0.0 denominator = 0.0 excluded: list[str] = [] seen: set[str] = set() for criterion, score, weight in args.criteria: if criterion in seen: parser.error(f"duplicate criterion ID: {criterion}") seen.add(criterion) if score is None: excluded.append(criterion) continue numerator += score * weight denominator += weight if denominator == 0: parser.error("no applicable criteria") raw = numerator / denominator cap = CAPS[args.gate] final = min(raw, cap) if cap is not None else raw print(f"gate = {args.gate.upper()}") print(f"applicable criteria = {len(args.criteria) - len(excluded)}") print(f"N/A = {', '.join(excluded) or 'none'}") print(f"sum(score x weight) = {numerator:g}") print(f"sum(weight) = {denominator:g}") print(f"raw score = {raw:.2f}") if cap is None: print("cap = none") else: print(f"cap = {cap:g} ({args.gate.upper()} gate)") print(f"final score = {final:.2f}") print(f"grade = {grade(final)}") return 0 if __name__ == "__main__": sys.exit(main())
-
-
SKILL.md 8.2 KB
--- name: agent-plugin-eval description: Audit, score, and compare repositories containing portable Agent Plugins against the official Agent Plugins specification. Use when asked to review a plugin repo, check plugin.json or mcp.json conformance, assess bundled skills and MCP servers, produce an evidence-cited 0–100 plugin scorecard, identify release blockers, or compare two agent plugins side by side. --- # Agent Plugin Evaluation Treat the portable Agent Plugins specification as the authority. A client-native manifest (e.g., `.codex-plugin/plugin.json`, `.claude/settings.json`, `.cursor/mcp.json`) does not replace the required root `plugin.json`. ## Parameters | Parameter | Description | Default | |---|---|---| | `target` | Local repository/plugin path or Git URL | Ask if missing | | `compare` | Optional second path or Git URL | None | | `output` | Scorecard destination | Reply only; write only when requested | | `spec_version` | Agent Plugins version to evaluate | Version declared by `plugin.json`, or `1.0.0` | ## Safety boundary Audit untrusted repositories statically. Do not run bundled executables, hooks, install scripts, package managers, MCP servers, or networked tests unless the user explicitly authorizes execution. Redact suspected secret values; report only their location and kind. A secret-like key or value is a suspicion, not confirmation: do not assign the `FAIL` gate without corroborating evidence such as a recognized live credential format, a trusted secret scanner, repository history/provenance, or user confirmation. Never test a credential against a service merely to confirm it. ## Workflow 1. **Resolve the plugin root.** Use a local target in place. For a Git URL, shallow-clone into a `mktemp -d` directory. A plugin root contains root `plugin.json`; if a repo has zero or multiple candidates, report the ambiguity instead of guessing. Done when every target maps to one explicit plugin root. 2. **Load the governing rules.** Read `references/spec-checklist.md` and `references/rubric.md`. For Agent Plugins `1.0.0`, use the bundled snapshot. For another declared version, or when the user asks for the latest spec, browse the canonical specification and schemas at `agent-plugins.org` and record the evaluated version and retrieval date. The normative text wins if it conflicts with JSON Schema. 3. **Inventory every package path.** Include dotfiles, symlinks, immediate skill children, extension namespaces, executable files, and files ignored by Git. Resolve every symlink and package-relative path against the plugin root. Done when every discovered path is accounted for as portable core, client extension, supporting file, or containment violation. 4. **Run the deterministic scan.** Execute `python3 scripts/inspect_plugin.py <plugin-root> --json`. Treat its output as evidence leads, not the final judgment. Confirm each reported issue in the source and add `file:line` or JSON-pointer evidence. Never weaken a normative finding merely because a client happens to accept it. 5. **Review components completely.** Inspect every immediate `skills/*/SKILL.md` and every `mcpServers` entry. Validate Agent Skills against their own specification. Assess instructions, resources, scripts, MCP configuration, extension isolation, cohesion, and practical utility. If `skill-evaluation` is available, it may deepen individual skill-quality analysis, but it never replaces this plugin-level rubric. 6. **Classify conformance before scoring.** Use the exact failure boundaries in `references/spec-checklist.md`: `PASS`, `PARTIAL`, or `FAIL`. Keep client compatibility separate from portable conformance. A client-specific feature may be excellent for that client and still add zero portable coverage. 7. **Score with cite-or-cut.** Score all applicable rubric criteria from `references/rubric.md`. Every score needs specific evidence; every `N/A` needs a reason. Run `scripts/score.py` for the weighted result and gate cap; do not calculate it by hand. Done when all criteria and all findings are reconciled with the conformance status. 8. **Answer in the requested language.** Read `references/output-template.md` and emit that structure. Lead with the verdict, distinguish blockers from recommendations, and provide concrete fixes. When `compare` is set, evaluate both independently before computing deltas; never force the same N/A set on both plugins. ## Gates and scoring - `PASS`: no normative violation found; no score cap. - `PARTIAL`: non-fatal manifest deviation or invalid/skipped component; final score capped at 59. - `FAIL`: fatal manifest/package-root failure, root-manifest escape, or confirmed embedded credential; final score capped at 39. - Keep the uncapped score visible so authors can distinguish design quality from release-blocking conformance. Invoke the calculator with one `criterion:score:weight` triple per criterion: ```bash python3 scripts/score.py --gate partial 1:90:3 2:80:3 3:NA:2 ``` ## Evaluation with Jev (Optional) When [TypeSafe Jev](https://github.com/AugmentHCI/typesafe-jev) is available, use it for subjective quality criteria. Jev provides calibrated probability judgments that augment the deterministic checks. ### When to use Jev | Evaluation Type | Use Jev? | Method | |-----------------|----------|--------| | Axes 1-3 conformance | No | Deterministic (`inspect_plugin.py`) | | Axis 4 product quality | **Yes** | Score (UX, docs, errors) | | Axis 2 quality criteria | **Yes** | Score (schema design, naming) | | Gate classification | **Yes** | Noul (pass/fail categories) | | Secret detection | **Yes** | Noul (suspected/not_suspected) | | Quality checklist | **Yes** | Noul (present/missing) | ### Discovery protocol ```python from typesafe import jev_available if jev_available(): from typesafe import Score, Noul # Use Jev for subjective criteria else: # Fall back to heuristic scoring ``` ### Questions and integration Questions are defined in `scripts/jev_questions.json`: - **7 Score questions**: Axis 4 (UX coherence, documentation clarity, error handling) and Axis 2 (validation, schema design, naming, API elegance) - **20 Noul questions**: Gates (G1-G4), secrets (4), quality checklist (7), component validity (4) Score results (0.0-1.0) are averaged per axis and scaled to the rubric (0-25). Noul results provide categorical classifications for gates and checklists. See `references/jev-integration.md` for full integration patterns and code examples. ### Output format When Jev is used, the scorecard includes a `jev` section: ```json { "jev": { "available": true, "quality_scores": { "ux_coherence": 0.72, ... }, "gate_classifications": { "G1": {"label": "conformant", "passed": true} }, "secret_findings": { "requires_review": false } } } ``` ## Gotchas - The v1 portable core contains exactly Agent Skills and MCP servers. Hooks, commands, agents, apps, marketplaces, and distribution policy are client-specific unless placed in a valid extension namespace. - Missing optional `skills/` or `mcp.json` is not an error. A present path of the wrong filesystem kind is an invalid component type. - Unknown root manifest fields are schema violations but have the spec's narrow non-fatal handling; most other manifest schema violations reject the whole plugin. - One invalid skill or MCP server must not be reported as if every independent component were invalid. - `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` expand only in MCP `args`, `env` values, and `cwd`; never in `command`, URLs, or headers. - A high-quality client-native plugin can still fail the portable standard when root `plugin.json` is absent. Report both facts without averaging them away. - Keep possible credentials labeled “suspected” and redacted. A heuristic hit alone lowers the security score and demands remediation review, but does not become a confirmed-credential `FAIL` gate. ## Final quality gate - [ ] Every target resolved to exactly one root - [ ] Every file, symlink, skill, MCP server, and extension inspected - [ ] Every normative violation mapped to its correct failure boundary - [ ] Every score cited and every N/A justified - [ ] Suspected secrets redacted - [ ] Score produced by `scripts/score.py` - [ ] Comparison deltas use independently computed scores
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.