@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 +71 -4
- package/dist/{chunk-ZR6XRGMQ.js → chunk-44R4ZYOE.js} +67 -0
- package/dist/chunk-AVF3GYCS.js +692 -0
- package/dist/{chunk-35UOPCBW.js → chunk-DN2OX4RT.js} +456 -2
- package/dist/{chunk-VOPISHBU.js → chunk-M5HXKZVN.js} +2 -2
- package/dist/cli-skill.d.ts +1 -0
- package/dist/cli-skill.js +98 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +441 -3
- package/dist/index.js +86 -8
- package/dist/mcp.js +130 -122
- package/dist/src-TG44QXFV.js +67 -0
- package/package.json +4 -4
- package/dist/chunk-LBXHFQN3.js +0 -219
- package/dist/src-RSTPCEYU.js +0 -31
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.
|
|
65
|
-
Claude Code
|
|
66
|
-
|
|
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
|
-
|
|
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
|
};
|