@polygraphso/litmus 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,9 +61,9 @@ MCP-capable client. It exposes two tools:
61
61
  It also registers two **prompts** that show up as slash commands — in Claude Code,
62
62
  `/mcp__polygraph-litmus__grade <server_ref>` (run a fresh grade) and
63
63
  `/mcp__polygraph-litmus__check <server_ref>` (read a published grade); other
64
- clients surface the same prompts in their own UI. (Want a bare `/polygraph` in
65
- Claude Code? Drop a `.claude/commands/polygraph.md` that calls `run_litmus`a
66
- Claude-Code-only convenience, not shipped here.)
64
+ clients surface the same prompts in their own UI. For a cleaner pair of commands
65
+ in Claude Code `/polygraph:grade` and `/polygraph:check` — install the plugin
66
+ (below), which wires up this server and both commands in one step.
67
67
 
68
68
  **Prerequisites:** Node ≥ 18. Docker is optional (without it, C-02 egress is
69
69
  skipped and the grade caps at B). Set `POLYGRAPH_API_URL=https://polygraph.so` so
@@ -73,7 +73,20 @@ skipped and the grade caps at B). Set `POLYGRAPH_API_URL=https://polygraph.so` s
73
73
  > commonly returns `not_available` today — that means *unevaluated*, not a failing
74
74
  > grade. To grade a server right now, use `run_litmus`.
75
75
 
76
- Add the server once, then just talk to your agent.
76
+ ### Claude Code: one-click plugin (recommended)
77
+
78
+ The plugin bundles this MCP server **and** adds the `/polygraph:grade` and
79
+ `/polygraph:check` commands — one install does everything:
80
+
81
+ ```
82
+ /plugin marketplace add polygraphso/litmus
83
+ /plugin install polygraph@polygraphso
84
+ ```
85
+
86
+ Then just run `/polygraph:grade npm/@modelcontextprotocol/server-filesystem`.
87
+
88
+ Prefer to wire the server up by hand, or using another client? Add it once, then
89
+ just talk to your agent.
77
90
 
78
91
  **Claude Code** — one command:
79
92
 
@@ -138,6 +151,54 @@ machine.
138
151
  - **`verify_attestation` says `lookup_failed`:** the grade index or RPC was
139
152
  unreachable — that's *unknown*, not *no grade*. Retry; check `POLYGRAPH_API_URL`.
140
153
 
154
+ ## Grade a skill
155
+
156
+ Claude Code / Agent **Skills** (a `SKILL.md` plus an optional bundle) are graded by a
157
+ separate static litmus (`litmus-skill-v1`). It scans the skill's bytes — **S-01**
158
+ prompt injection in the body, **S-03** data-exfiltration instructions, **S-04**
159
+ dangerous commands in bundled executable scripts — and content-hashes the whole
160
+ directory. The letter is **A/B/D/F**.
161
+
162
+ This is a **static** scan: it does not execute the skill or its scripts, so an `A`
163
+ means the static checks were clean, not that the skill is behaviorally safe. A
164
+ command the skill builds or fetches at runtime is not visible to it.
165
+
166
+ ### CLI
167
+
168
+ ```bash
169
+ polygraphso-litmus-skill <path-to-skill-dir> # grade a local skill folder (must contain SKILL.md)
170
+ polygraphso-litmus-skill --json <path-to-skill-dir> # machine-readable safety + quality bundles
171
+ ```
172
+
173
+ It also prints a separate, advisory **quality** signal (`well-formed` / `issues` /
174
+ `malformed`) — never an A–F letter, never minted. Its deterministic checks
175
+ (frontmatter + bundled-link resolution) always run; the optional LLM-judged axes
176
+ (honesty, coherence) run only when a judge is available:
177
+
178
+ - **Inside an agent** (the MCP tool below): the host agent's own model judges via MCP
179
+ sampling — no key, any provider.
180
+ - **Standalone:** bring your own key for any OpenAI-compatible endpoint:
181
+
182
+ ```bash
183
+ export LITMUS_LLM_API_KEY=… # your key
184
+ export LITMUS_LLM_MODEL=gpt-4o # any model the endpoint serves
185
+ export LITMUS_LLM_BASE_URL=https://api.openai.com/v1 # optional; defaults to OpenAI
186
+ ```
187
+
188
+ - With neither, the judged axes are skipped — the grade and deterministic quality
189
+ still run. The core never needs a key.
190
+
191
+ ### From an AI agent (MCP)
192
+
193
+ The same `polygraphso-litmus-mcp` server exposes two skill tools (plus `grade-skill` /
194
+ `check-skill` prompts):
195
+
196
+ - **`run_skill_litmus`** — grade a local skill directory now (static; uses the host
197
+ model via sampling for the quality axes, no key).
198
+ - **`verify_skill_attestation`** — read a skill's *already-published* grade. It returns
199
+ the attested `contentHash`; recompute the skill's hash and require equality before
200
+ installing — the content hash, not the version, is the trust anchor.
201
+
141
202
  ## Library
142
203
 
143
204
  ```ts
@@ -145,6 +206,12 @@ import { runLitmus, gateDecision, liveFingerprint, readAttestation } from "@poly
145
206
 
146
207
  const bundle = await runLitmus("npm/@modelcontextprotocol/server-filesystem");
147
208
  console.log(bundle.grade, bundle.gradeRationale);
209
+
210
+ // Skills: static safety grade + a separate advisory quality bundle.
211
+ import { runSkillLitmus, runSkillQuality } from "@polygraphso/litmus";
212
+
213
+ const skill = runSkillLitmus("./skills/my-skill");
214
+ console.log(skill.grade, skill.contentHash);
148
215
  ```
149
216
 
150
217
  ## License
@@ -82,6 +82,69 @@ function serverKey(parts) {
82
82
  return parts.owner ? `${parts.registry}/${parts.owner}/${parts.name}` : `${parts.registry}/${parts.name}`;
83
83
  }
84
84
 
85
+ // ../core/src/skill-identity.ts
86
+ var SOURCES = /* @__PURE__ */ new Set(["github", "marketplace", "npm"]);
87
+ var OWNER_RE2 = /^@?[A-Za-z0-9][A-Za-z0-9._-]*$/;
88
+ var NAME_RE2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
89
+ var REF_RE = /^[A-Za-z0-9][A-Za-z0-9.+_-]*$/;
90
+ var PATH_SEG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
91
+ var SkillRefParseError = class extends Error {
92
+ constructor(ref, reason) {
93
+ super(`Invalid skill ref "${ref}": ${reason}`);
94
+ this.name = "SkillRefParseError";
95
+ }
96
+ };
97
+ function parseSkillRef(ref) {
98
+ const firstSlash = ref.indexOf("/");
99
+ if (firstSlash === -1) throw new SkillRefParseError(ref, "expected `{source}/...`");
100
+ const source = ref.slice(0, firstSlash);
101
+ if (!SOURCES.has(source)) {
102
+ throw new SkillRefParseError(ref, `unknown source "${source}" (expected one of: ${[...SOURCES].join(", ")})`);
103
+ }
104
+ let rest = ref.slice(firstSlash + 1);
105
+ let pin = null;
106
+ const at = rest.lastIndexOf("@");
107
+ if (at > 0) {
108
+ pin = rest.slice(at + 1);
109
+ rest = rest.slice(0, at);
110
+ if (!pin) throw new SkillRefParseError(ref, "empty ref after `@`");
111
+ if (!REF_RE.test(pin)) throw new SkillRefParseError(ref, "ref contains disallowed characters");
112
+ }
113
+ let path = null;
114
+ const hash = rest.indexOf("#");
115
+ if (hash >= 0) {
116
+ path = rest.slice(hash + 1);
117
+ rest = rest.slice(0, hash);
118
+ if (!path) throw new SkillRefParseError(ref, "empty path after `#`");
119
+ for (const seg of path.split("/")) {
120
+ if (!PATH_SEG_RE.test(seg)) throw new SkillRefParseError(ref, "path contains disallowed characters");
121
+ }
122
+ }
123
+ const lastSlash = rest.lastIndexOf("/");
124
+ let owner;
125
+ let name;
126
+ if (lastSlash === -1) {
127
+ owner = null;
128
+ name = rest;
129
+ } else {
130
+ owner = rest.slice(0, lastSlash);
131
+ name = rest.slice(lastSlash + 1);
132
+ }
133
+ if (!name) throw new SkillRefParseError(ref, "empty name segment");
134
+ if (owner !== null && !OWNER_RE2.test(owner)) throw new SkillRefParseError(ref, "owner contains disallowed characters");
135
+ if (!NAME_RE2.test(name)) throw new SkillRefParseError(ref, "name contains disallowed characters");
136
+ return { source, owner, name, path, ref: pin };
137
+ }
138
+ function formatSkillRef(p) {
139
+ let base = p.owner ? `${p.source}/${p.owner}/${p.name}` : `${p.source}/${p.name}`;
140
+ if (p.path) base += `#${p.path}`;
141
+ return p.ref ? `${base}@${p.ref}` : base;
142
+ }
143
+ function skillKey(p) {
144
+ const base = p.owner ? `${p.source}/${p.owner}/${p.name}` : `${p.source}/${p.name}`;
145
+ return p.path ? `${base}#${p.path}` : base;
146
+ }
147
+
85
148
  // ../core/src/canonical.ts
86
149
  function canonicalStringify(value) {
87
150
  return JSON.stringify(sortDeep(value));
@@ -116,5 +179,9 @@ export {
116
179
  parseServerRef,
117
180
  formatServerRef,
118
181
  serverKey,
182
+ SkillRefParseError,
183
+ parseSkillRef,
184
+ formatSkillRef,
185
+ skillKey,
119
186
  canonicalStringify
120
187
  };