@proofofwork-agency/toolpin 0.2.3
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/CONTRIBUTING.md +117 -0
- package/LICENSE +183 -0
- package/README.md +323 -0
- package/SECURITY.md +61 -0
- package/action.yml +134 -0
- package/dist/canonicalJson.js +38 -0
- package/dist/capabilities.js +139 -0
- package/dist/ci.js +26 -0
- package/dist/cli.js +1843 -0
- package/dist/clientSupport.js +76 -0
- package/dist/codexToml.js +213 -0
- package/dist/config.js +337 -0
- package/dist/constants.js +3 -0
- package/dist/continueYaml.js +76 -0
- package/dist/doctor.js +163 -0
- package/dist/install.js +191 -0
- package/dist/installed.js +405 -0
- package/dist/integrity.js +14 -0
- package/dist/inventory.js +169 -0
- package/dist/packageIntegrity.js +153 -0
- package/dist/plan.js +595 -0
- package/dist/policy.js +310 -0
- package/dist/registry.js +1610 -0
- package/dist/runtimeAdvisory.js +80 -0
- package/dist/safeFetch.js +157 -0
- package/dist/sarif.js +162 -0
- package/dist/scan.js +113 -0
- package/dist/search.js +44 -0
- package/dist/secrets.js +165 -0
- package/dist/signing.js +146 -0
- package/dist/tester.js +240 -0
- package/dist/trust.js +528 -0
- package/dist/tui/app.js +1731 -0
- package/dist/tui/command.js +50 -0
- package/dist/tui/configSnippet.js +11 -0
- package/dist/tui/constants.js +37 -0
- package/dist/tui/format.js +31 -0
- package/dist/tui/installedState.js +23 -0
- package/dist/tui/layout.js +65 -0
- package/dist/tui/selectors.js +282 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui/ui/trust.js +77 -0
- package/dist/tui/views/installed.js +82 -0
- package/dist/tui/views/panels.js +637 -0
- package/dist/tui.js +12 -0
- package/dist/types.js +1 -0
- package/dist/verificationTrust.js +103 -0
- package/dist/verify.js +537 -0
- package/dist/version.js +1 -0
- package/dist/versions.js +127 -0
- package/docs/assets/readme/terminal-demo.svg +174 -0
- package/docs/assets/readme/tui-browse-overview.jpg +0 -0
- package/docs/assets/readme/tui-config-preview.jpg +0 -0
- package/docs/assets/readme/tui-help.jpg +0 -0
- package/docs/assets/readme/tui-installed-inventory.jpg +0 -0
- package/docs/how-to/catch-drift-in-ci.md +189 -0
- package/docs/how-to/custom-registries.md +156 -0
- package/docs/how-to/toolpin-curated-registry.md +153 -0
- package/package.json +76 -0
- package/registry/README.md +92 -0
- package/registry/v0/servers +115 -0
package/dist/versions.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export function knownVersions(servers, name) {
|
|
2
|
+
const byVersion = new Map();
|
|
3
|
+
for (const server of servers) {
|
|
4
|
+
if (server.name !== name)
|
|
5
|
+
continue;
|
|
6
|
+
const existing = byVersion.get(server.version);
|
|
7
|
+
if (!existing || server.isLatest) {
|
|
8
|
+
byVersion.set(server.version, {
|
|
9
|
+
version: server.version,
|
|
10
|
+
source: server.registrySource,
|
|
11
|
+
isLatest: server.isLatest,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const sorted = [...byVersion.values()].sort(compareKnownVersions);
|
|
16
|
+
return sorted.map((entry, index) => ({ ...entry, isLatest: entry.isLatest || index === 0 }));
|
|
17
|
+
}
|
|
18
|
+
export function latestKnownVersion(servers, name) {
|
|
19
|
+
return knownVersions(servers, name)[0];
|
|
20
|
+
}
|
|
21
|
+
export function compareLockedToLatest(name, lockedVersion, servers) {
|
|
22
|
+
const versions = knownVersions(servers, name);
|
|
23
|
+
const latest = versions[0];
|
|
24
|
+
if (!lockedVersion || !latest) {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
lockedVersion,
|
|
28
|
+
latestVersion: latest?.version,
|
|
29
|
+
status: "unknown",
|
|
30
|
+
previousVersions: versions.slice(1),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const delta = compareSemver(latest.version, lockedVersion);
|
|
34
|
+
return {
|
|
35
|
+
name,
|
|
36
|
+
lockedVersion,
|
|
37
|
+
latestVersion: latest.version,
|
|
38
|
+
status: delta === undefined
|
|
39
|
+
? "unknown"
|
|
40
|
+
: delta > 0
|
|
41
|
+
? "update-available"
|
|
42
|
+
: delta < 0
|
|
43
|
+
? "ahead-of-registry"
|
|
44
|
+
: "current",
|
|
45
|
+
previousVersions: versions.filter((entry) => entry.version !== latest.version),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function compareVersionish(a, b) {
|
|
49
|
+
return compareSemver(a, b) ?? 0;
|
|
50
|
+
}
|
|
51
|
+
export function compareVersionStatus(a, b) {
|
|
52
|
+
return compareSemver(a, b);
|
|
53
|
+
}
|
|
54
|
+
function compareKnownVersions(left, right) {
|
|
55
|
+
const semverDelta = compareSemver(right.version, left.version);
|
|
56
|
+
if (semverDelta !== undefined && semverDelta !== 0)
|
|
57
|
+
return semverDelta;
|
|
58
|
+
if (left.isLatest !== right.isLatest)
|
|
59
|
+
return left.isLatest ? -1 : 1;
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
const SEMVER_PATTERN = new RegExp("^v?"
|
|
63
|
+
+ "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)"
|
|
64
|
+
+ "(?:-((?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?"
|
|
65
|
+
+ "(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?"
|
|
66
|
+
+ "$");
|
|
67
|
+
function compareSemver(a, b) {
|
|
68
|
+
const left = parseSemver(a);
|
|
69
|
+
const right = parseSemver(b);
|
|
70
|
+
if (!left || !right)
|
|
71
|
+
return undefined;
|
|
72
|
+
const versionDelta = compareNumbers(left.major, right.major)
|
|
73
|
+
|| compareNumbers(left.minor, right.minor)
|
|
74
|
+
|| compareNumbers(left.patch, right.patch);
|
|
75
|
+
if (versionDelta !== 0)
|
|
76
|
+
return versionDelta;
|
|
77
|
+
return comparePrerelease(left.prerelease, right.prerelease);
|
|
78
|
+
}
|
|
79
|
+
function parseSemver(version) {
|
|
80
|
+
const match = SEMVER_PATTERN.exec(version);
|
|
81
|
+
if (!match)
|
|
82
|
+
return undefined;
|
|
83
|
+
return {
|
|
84
|
+
major: Number.parseInt(match[1], 10),
|
|
85
|
+
minor: Number.parseInt(match[2], 10),
|
|
86
|
+
patch: Number.parseInt(match[3], 10),
|
|
87
|
+
prerelease: match[4]?.split(".") ?? [],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function comparePrerelease(left, right) {
|
|
91
|
+
if (left.length === 0 && right.length === 0)
|
|
92
|
+
return 0;
|
|
93
|
+
if (left.length === 0)
|
|
94
|
+
return 1;
|
|
95
|
+
if (right.length === 0)
|
|
96
|
+
return -1;
|
|
97
|
+
const length = Math.max(left.length, right.length);
|
|
98
|
+
for (let i = 0; i < length; i += 1) {
|
|
99
|
+
const leftIdentifier = left[i];
|
|
100
|
+
const rightIdentifier = right[i];
|
|
101
|
+
if (leftIdentifier === undefined)
|
|
102
|
+
return -1;
|
|
103
|
+
if (rightIdentifier === undefined)
|
|
104
|
+
return 1;
|
|
105
|
+
const identifierDelta = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier);
|
|
106
|
+
if (identifierDelta !== 0)
|
|
107
|
+
return identifierDelta;
|
|
108
|
+
}
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
function comparePrereleaseIdentifier(left, right) {
|
|
112
|
+
const leftNumeric = isNumericIdentifier(left);
|
|
113
|
+
const rightNumeric = isNumericIdentifier(right);
|
|
114
|
+
if (leftNumeric && rightNumeric)
|
|
115
|
+
return compareNumbers(Number.parseInt(left, 10), Number.parseInt(right, 10));
|
|
116
|
+
if (leftNumeric)
|
|
117
|
+
return -1;
|
|
118
|
+
if (rightNumeric)
|
|
119
|
+
return 1;
|
|
120
|
+
return left === right ? 0 : left > right ? 1 : -1;
|
|
121
|
+
}
|
|
122
|
+
function compareNumbers(left, right) {
|
|
123
|
+
return left === right ? 0 : left > right ? 1 : -1;
|
|
124
|
+
}
|
|
125
|
+
function isNumericIdentifier(value) {
|
|
126
|
+
return /^(0|[1-9]\d*)$/.test(value);
|
|
127
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="560" viewBox="0 0 1200 560" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Animated ToolPin terminal workflow</title>
|
|
3
|
+
<desc id="desc">A looping terminal animation showing ToolPin search, plan, install, lock, and CI drift-check commands.</desc>
|
|
4
|
+
<style>
|
|
5
|
+
:root {
|
|
6
|
+
color-scheme: dark;
|
|
7
|
+
}
|
|
8
|
+
svg {
|
|
9
|
+
background: #111;
|
|
10
|
+
}
|
|
11
|
+
.window {
|
|
12
|
+
fill: #181818;
|
|
13
|
+
stroke: #303030;
|
|
14
|
+
stroke-width: 1;
|
|
15
|
+
}
|
|
16
|
+
.topbar {
|
|
17
|
+
fill: #222;
|
|
18
|
+
}
|
|
19
|
+
.dot-red {
|
|
20
|
+
fill: #ff5f57;
|
|
21
|
+
}
|
|
22
|
+
.dot-yellow {
|
|
23
|
+
fill: #ffbd2e;
|
|
24
|
+
}
|
|
25
|
+
.dot-green {
|
|
26
|
+
fill: #28c840;
|
|
27
|
+
}
|
|
28
|
+
text {
|
|
29
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
30
|
+
font-size: 24px;
|
|
31
|
+
fill: #d8d8d8;
|
|
32
|
+
}
|
|
33
|
+
.small {
|
|
34
|
+
font-size: 19px;
|
|
35
|
+
fill: #999;
|
|
36
|
+
}
|
|
37
|
+
.prompt {
|
|
38
|
+
fill: #70e1bd;
|
|
39
|
+
font-weight: 700;
|
|
40
|
+
}
|
|
41
|
+
.cmd {
|
|
42
|
+
fill: #f5f5f5;
|
|
43
|
+
}
|
|
44
|
+
.muted {
|
|
45
|
+
fill: #9a9a9a;
|
|
46
|
+
}
|
|
47
|
+
.ok {
|
|
48
|
+
fill: #70e1bd;
|
|
49
|
+
font-weight: 700;
|
|
50
|
+
}
|
|
51
|
+
.warn {
|
|
52
|
+
fill: #ffd75f;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
}
|
|
55
|
+
.blue {
|
|
56
|
+
fill: #7cc7ff;
|
|
57
|
+
font-weight: 700;
|
|
58
|
+
}
|
|
59
|
+
.bar-ok {
|
|
60
|
+
fill: #70e1bd;
|
|
61
|
+
}
|
|
62
|
+
.bar-warn {
|
|
63
|
+
fill: #ffd75f;
|
|
64
|
+
}
|
|
65
|
+
.bar-bg {
|
|
66
|
+
fill: #3a3a3a;
|
|
67
|
+
}
|
|
68
|
+
.cursor {
|
|
69
|
+
fill: #70e1bd;
|
|
70
|
+
animation: blink 1s steps(2, start) infinite;
|
|
71
|
+
}
|
|
72
|
+
.frame {
|
|
73
|
+
opacity: 0;
|
|
74
|
+
animation: frames 12s steps(1, end) infinite;
|
|
75
|
+
}
|
|
76
|
+
.f1 {
|
|
77
|
+
animation-delay: 0s;
|
|
78
|
+
}
|
|
79
|
+
.f2 {
|
|
80
|
+
animation-delay: -9.6s;
|
|
81
|
+
}
|
|
82
|
+
.f3 {
|
|
83
|
+
animation-delay: -7.2s;
|
|
84
|
+
}
|
|
85
|
+
.f4 {
|
|
86
|
+
animation-delay: -4.8s;
|
|
87
|
+
}
|
|
88
|
+
.f5 {
|
|
89
|
+
animation-delay: -2.4s;
|
|
90
|
+
}
|
|
91
|
+
@keyframes frames {
|
|
92
|
+
0%, 19.99% {
|
|
93
|
+
opacity: 1;
|
|
94
|
+
}
|
|
95
|
+
20%, 100% {
|
|
96
|
+
opacity: 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
@keyframes blink {
|
|
100
|
+
0%, 45% {
|
|
101
|
+
opacity: 1;
|
|
102
|
+
}
|
|
103
|
+
46%, 100% {
|
|
104
|
+
opacity: 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
108
|
+
|
|
109
|
+
<rect class="window" x="24" y="24" width="1152" height="512" rx="14"/>
|
|
110
|
+
<rect class="topbar" x="24" y="24" width="1152" height="58" rx="14"/>
|
|
111
|
+
<rect class="topbar" x="24" y="62" width="1152" height="20"/>
|
|
112
|
+
<circle class="dot-red" cx="60" cy="53" r="9"/>
|
|
113
|
+
<circle class="dot-yellow" cx="90" cy="53" r="9"/>
|
|
114
|
+
<circle class="dot-green" cx="120" cy="53" r="9"/>
|
|
115
|
+
<text x="160" y="61" class="small">ToolPin v0.2.2 - MCP install review gate - toolpin or tpn</text>
|
|
116
|
+
|
|
117
|
+
<g class="frame f1">
|
|
118
|
+
<text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin search contextrelay --source toolpin</tspan></text>
|
|
119
|
+
<text x="62" y="174" class="muted">loaded 1 ToolPin-curated server</text>
|
|
120
|
+
<text x="62" y="222"><tspan class="ok">toolpin</tspan> ContextRelay <tspan class="ok">EVIDENCE OK</tspan></text>
|
|
121
|
+
<text x="62" y="270" class="muted">@proofofwork-agency/contextrelay@3.9.2</text>
|
|
122
|
+
<text x="62" y="318" class="muted">Local coordination bridge for Codex and Claude Code agents.</text>
|
|
123
|
+
<rect class="bar-ok" x="62" y="370" width="390" height="22" rx="4"/>
|
|
124
|
+
<rect class="bar-bg" x="452" y="370" width="84" height="22" rx="4"/>
|
|
125
|
+
<text x="62" y="440"><tspan class="prompt">></tspan> select, review, then install only after the plan looks right</text>
|
|
126
|
+
<rect class="cursor" x="1070" y="418" width="14" height="28"/>
|
|
127
|
+
</g>
|
|
128
|
+
|
|
129
|
+
<g class="frame f2">
|
|
130
|
+
<text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin plan @proofofwork-agency/contextrelay --client codex --live --verify</tspan></text>
|
|
131
|
+
<text x="62" y="178"><tspan class="blue">overview</tspan> @proofofwork-agency/contextrelay@3.9.2</text>
|
|
132
|
+
<text x="62" y="226">runtime npm transport stdio secrets <tspan class="ok">none declared</tspan></text>
|
|
133
|
+
<text x="62" y="274">evidence <tspan class="ok">ToolPin-verified npm_integrity_verified</tspan></text>
|
|
134
|
+
<text x="62" y="322">badges source repo, namespaced, npm, pinned version</text>
|
|
135
|
+
<text x="62" y="382">profile</text>
|
|
136
|
+
<rect class="bar-ok" x="190" y="360" width="380" height="22" rx="4"/>
|
|
137
|
+
<text x="596" y="382"><tspan class="ok">100%</tspan> metadata profile</text>
|
|
138
|
+
<text x="62" y="436">integrity</text>
|
|
139
|
+
<rect class="bar-ok" x="190" y="414" width="380" height="22" rx="4"/>
|
|
140
|
+
<text x="596" y="436"><tspan class="ok">100%</tspan> artifact evidence</text>
|
|
141
|
+
</g>
|
|
142
|
+
|
|
143
|
+
<g class="frame f3">
|
|
144
|
+
<text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin install @proofofwork-agency/contextrelay --client codex --scope global --update-lock</tspan></text>
|
|
145
|
+
<text x="62" y="184"><tspan class="blue">Install</tspan></text>
|
|
146
|
+
<text x="62" y="232">server @proofofwork-agency/contextrelay@3.9.2</text>
|
|
147
|
+
<text x="62" y="280">client codex scope global</text>
|
|
148
|
+
<text x="62" y="328">config <tspan class="ok">updated</tspan>: ~/.codex/config.toml</text>
|
|
149
|
+
<text x="62" y="376">lock <tspan class="ok">mcp-lock.json updated</tspan></text>
|
|
150
|
+
<text x="62" y="424" class="muted">- Requires Node.js and npm/npx on PATH.</text>
|
|
151
|
+
<text x="62" y="472"><tspan class="ok">done</tspan> installed for codex</text>
|
|
152
|
+
</g>
|
|
153
|
+
|
|
154
|
+
<g class="frame f4">
|
|
155
|
+
<text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin audit --verify</tspan></text>
|
|
156
|
+
<text x="62" y="184"><tspan class="blue">Audit findings</tspan></text>
|
|
157
|
+
<text x="62" y="232">lockfile mcp-lock.json</text>
|
|
158
|
+
<text x="62" y="280">checked 1 locked, 15 config file(s)</text>
|
|
159
|
+
<text x="62" y="328">doctor <tspan class="ok">ok</tspan> installed config matches lockfile</text>
|
|
160
|
+
<text x="62" y="376">secrets <tspan class="ok">ok</tspan> no plaintext secret findings</text>
|
|
161
|
+
<text x="62" y="424">evidence <tspan class="ok">ok</tspan> trusted artifact proof is fresh</text>
|
|
162
|
+
<text x="62" y="472"><tspan class="ok">no findings</tspan></text>
|
|
163
|
+
</g>
|
|
164
|
+
|
|
165
|
+
<g class="frame f5">
|
|
166
|
+
<text x="62" y="126"><tspan class="prompt">$</tspan><tspan class="cmd"> toolpin ci --file mcp-lock.json --live --verify</tspan></text>
|
|
167
|
+
<text x="62" y="184">resolving locked server plans...</text>
|
|
168
|
+
<text x="62" y="232">checking policy, generated config, manifest, evidence...</text>
|
|
169
|
+
<text x="62" y="280">comparing reviewed lock entries...</text>
|
|
170
|
+
<text x="62" y="348"><tspan class="ok">PASS</tspan> mcp-lock.json matches reviewed install state</text>
|
|
171
|
+
<text x="62" y="408"><tspan class="warn">commit</tspan> mcp-lock.json and run this in pull requests</text>
|
|
172
|
+
<text x="62" y="468"><tspan class="prompt">$</tspan><tspan class="cmd"> git push </tspan><tspan class="muted"># ToolPin protects the PR</tspan></text>
|
|
173
|
+
</g>
|
|
174
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Catch Drift in CI
|
|
2
|
+
|
|
3
|
+
Use `toolpin ci` when `mcp-lock.json` is committed to a repository and pull
|
|
4
|
+
requests should fail if registry metadata, generated client config, policy, or
|
|
5
|
+
optional signatures no longer match the reviewed lockfile.
|
|
6
|
+
|
|
7
|
+
`toolpin ci` is read-only. It re-resolves locked entries, rebuilds install
|
|
8
|
+
plans, checks lock integrity, enforces policy unless bypassed, and exits
|
|
9
|
+
non-zero on drift. It does not update `mcp-lock.json`.
|
|
10
|
+
|
|
11
|
+
`toolpin ci --sarif` emits SARIF 2.1.0 JSON to stdout and still exits non-zero
|
|
12
|
+
on drift:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
toolpin ci --file mcp-lock.json --live --sarif > toolpin.sarif
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Automatic GitHub code-scanning upload is not wired into the composite action in
|
|
19
|
+
this pass; add an explicit upload step after reviewing the desired repository
|
|
20
|
+
permissions.
|
|
21
|
+
|
|
22
|
+
## Basic GitHub Action
|
|
23
|
+
|
|
24
|
+
After the repository is public and tagged, call the composite action from your
|
|
25
|
+
workflow. The action installs ToolPin from the action source by default, so it
|
|
26
|
+
does not require npm publish:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
name: ToolPin
|
|
30
|
+
|
|
31
|
+
on:
|
|
32
|
+
pull_request:
|
|
33
|
+
push:
|
|
34
|
+
branches: [main]
|
|
35
|
+
|
|
36
|
+
permissions:
|
|
37
|
+
contents: read
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
mcp-lock:
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
- uses: proofofwork-agency/toolpin@v0.2.3
|
|
45
|
+
with:
|
|
46
|
+
file: mcp-lock.json
|
|
47
|
+
live: "true"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The action builds ToolPin from `$GITHUB_ACTION_PATH` and runs:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
toolpin ci --file mcp-lock.json --source all --live --policy .toolpin/policy.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When `.toolpin/policy.json` is absent, the current CLI treats policy enforcement
|
|
57
|
+
as a no-op.
|
|
58
|
+
|
|
59
|
+
## Direct CLI Workflow
|
|
60
|
+
|
|
61
|
+
Use this form when you want the workflow to install the npm package directly
|
|
62
|
+
instead of using the composite Action:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
name: ToolPin
|
|
66
|
+
|
|
67
|
+
on:
|
|
68
|
+
pull_request:
|
|
69
|
+
|
|
70
|
+
permissions:
|
|
71
|
+
contents: read
|
|
72
|
+
|
|
73
|
+
jobs:
|
|
74
|
+
mcp-lock:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v4
|
|
78
|
+
- uses: actions/setup-node@v4
|
|
79
|
+
with:
|
|
80
|
+
node-version: 22
|
|
81
|
+
- run: npm install -g @proofofwork-agency/toolpin
|
|
82
|
+
- run: toolpin ci --file mcp-lock.json --live
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For unreleased source-checkout development, run `npm ci`, `npm test`, and
|
|
86
|
+
`npm run dev -- ci --file mcp-lock.json --live`.
|
|
87
|
+
|
|
88
|
+
## Digest Pin
|
|
89
|
+
|
|
90
|
+
`--expect-digest` compares the whole-lock digest against a value provided by
|
|
91
|
+
CI. Store the expected digest outside the pull request being checked, for
|
|
92
|
+
example as a GitHub Actions variable or secret.
|
|
93
|
+
|
|
94
|
+
Generate the digest after reviewing a lockfile change:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
toolpin lock digest --file mcp-lock.json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use it in CI:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
- uses: proofofwork-agency/toolpin@v0.2.3
|
|
104
|
+
with:
|
|
105
|
+
file: mcp-lock.json
|
|
106
|
+
live: "true"
|
|
107
|
+
expect-digest: ${{ vars.TOOLPIN_LOCK_DIGEST }}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Do not commit the expected digest next to `mcp-lock.json`; a PR that changes
|
|
111
|
+
both files would defeat the check.
|
|
112
|
+
|
|
113
|
+
## Detached Signature
|
|
114
|
+
|
|
115
|
+
ToolPin can verify a detached Ed25519 signature before registry resolution:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
toolpin lock sign --policy .toolpin/policy.json --key private.pem --file mcp-lock.json --signature mcp-lock.sig
|
|
119
|
+
toolpin lock verify-signature --policy .toolpin/policy.json --key public.pem --file mcp-lock.json --signature mcp-lock.sig
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Then in CI:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
- uses: proofofwork-agency/toolpin@v0.2.3
|
|
126
|
+
with:
|
|
127
|
+
file: mcp-lock.json
|
|
128
|
+
live: "true"
|
|
129
|
+
signature: mcp-lock.sig
|
|
130
|
+
public-key: public.pem
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Commit `mcp-lock.sig` and the public key only after review. Never commit the
|
|
134
|
+
private key. A signature is meaningful only when the private key and public
|
|
135
|
+
trust root are controlled outside the PR path.
|
|
136
|
+
|
|
137
|
+
## Policy and Live Verification
|
|
138
|
+
|
|
139
|
+
To enforce a non-default policy path:
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
- uses: proofofwork-agency/toolpin@v0.2.3
|
|
143
|
+
with:
|
|
144
|
+
policy: security/toolpin-policy.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To make CI skip policy enforcement explicitly:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- uses: proofofwork-agency/toolpin@v0.2.3
|
|
151
|
+
with:
|
|
152
|
+
no-policy: "true"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
To re-run verification before comparing locked plans, use the stricter CI
|
|
156
|
+
posture:
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
- uses: proofofwork-agency/toolpin@v0.2.3
|
|
160
|
+
with:
|
|
161
|
+
live: "true"
|
|
162
|
+
verify: "true"
|
|
163
|
+
timeout: "15000"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`verify: "true"` can require network access, local runtimes, and server
|
|
167
|
+
credentials for live MCP probes. Use `skip-live-verification: "true"` when you
|
|
168
|
+
want artifact/metadata verification without live `tools/list` probing. Treat
|
|
169
|
+
that as a conscious downgrade: it skips capability hashing and CI rejects it for
|
|
170
|
+
entries that already have live capability pins.
|
|
171
|
+
|
|
172
|
+
## What Fails the Build
|
|
173
|
+
|
|
174
|
+
CI exits non-zero when:
|
|
175
|
+
|
|
176
|
+
- `mcp-lock.json` is missing, empty, malformed, or has an unsupported version.
|
|
177
|
+
- Per-entry lock integrity is missing or invalid.
|
|
178
|
+
- A locked server/client no longer resolves to the reviewed install plan
|
|
179
|
+
(version, target, trust, generated config, or capability manifest drifted).
|
|
180
|
+
- The whole-lock digest (`--expect-digest`) does not match.
|
|
181
|
+
- Detached signature verification fails, or `signature` and `public-key` are not
|
|
182
|
+
supplied as a pair.
|
|
183
|
+
- The selected policy rejects a locked entry.
|
|
184
|
+
- `--verify` finds critical verification findings.
|
|
185
|
+
- `--sarif` changes the output format only; it does not make failing checks
|
|
186
|
+
advisory.
|
|
187
|
+
|
|
188
|
+
Use `toolpin install --update-lock` or `toolpin lock <server> --client <client>`
|
|
189
|
+
only after reviewing the drift locally. CI should not update the lockfile.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Custom Registries
|
|
2
|
+
|
|
3
|
+
ToolPin can read built-in registries and repo-owned registry definitions from `.toolpin/registries.json`.
|
|
4
|
+
|
|
5
|
+
Built-in sources:
|
|
6
|
+
|
|
7
|
+
- `toolpin`: ToolPin hosted curated registry with bundled fallback, installable,
|
|
8
|
+
`curated` trust, pinned, and always enabled.
|
|
9
|
+
- `official`: Official MCP Registry, installable, `canonical` trust.
|
|
10
|
+
- `docker`: Docker MCP Catalog, installable, `curated` trust.
|
|
11
|
+
- `pulsemcp`, `smithery`, `glama`: known directory sources, disabled by default. Enable one explicitly before it appears in `--source all`, browse, or search. `pulsemcp` is auth-gated, Smithery hosted targets require `--allow-hosted-directory-targets`, and Glama entries become installable only when ToolPin can match their repository to an Official MCP Registry entry with lockable targets.
|
|
12
|
+
|
|
13
|
+
Use source preferences to turn optional built-ins on or off. The pinned
|
|
14
|
+
`toolpin` source is always enabled:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
toolpin registry list
|
|
18
|
+
toolpin registry enable glama
|
|
19
|
+
toolpin registry disable glama
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Those commands write a top-level `sources` preference block:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"sources": {
|
|
27
|
+
"glama": {
|
|
28
|
+
"enabled": true
|
|
29
|
+
},
|
|
30
|
+
"smithery": {
|
|
31
|
+
"enabled": false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"registries": []
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
ToolPin also maintains a GitHub-backed curated registry. Current ToolPin
|
|
39
|
+
versions expose it as the built-in `toolpin` source and fetch
|
|
40
|
+
`https://raw.githubusercontent.com/proofofwork-agency/toolpin/main/registry/v0/servers`
|
|
41
|
+
first, falling back to the bundled snapshot if the hosted file is unavailable
|
|
42
|
+
or invalid.
|
|
43
|
+
|
|
44
|
+
See [ToolPin Curated Registry](./toolpin-curated-registry.md) for the PR-based
|
|
45
|
+
review workflow.
|
|
46
|
+
|
|
47
|
+
## Official-Compatible Registry
|
|
48
|
+
|
|
49
|
+
Use this for company or private registries that expose the MCP Registry `/v0/servers` response shape.
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"registries": [
|
|
54
|
+
{
|
|
55
|
+
"id": "company",
|
|
56
|
+
"type": "official-compatible",
|
|
57
|
+
"url": "https://registry.company.com/v0",
|
|
58
|
+
"mode": "installable",
|
|
59
|
+
"trust": "private"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then run:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
toolpin registry list
|
|
69
|
+
toolpin ingest --source company
|
|
70
|
+
toolpin search postgres --source company
|
|
71
|
+
toolpin install company/postgres --client claude --update-lock
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Installable entries still need enough machine-readable metadata for ToolPin to build a lockable install plan: a package or remote target, version, transport, source metadata, and any declared secrets.
|
|
75
|
+
|
|
76
|
+
Verified metadata comes from an installable source that exposes reviewable MCP
|
|
77
|
+
Registry-style server records, such as the Official MCP Registry, Docker MCP
|
|
78
|
+
Catalog, or an `official-compatible` custom/curated registry. For a directory
|
|
79
|
+
result to become installable, curate it into metadata with package/remotes,
|
|
80
|
+
exact versions, repository URLs, and artifact pins where available, such as OCI
|
|
81
|
+
`@sha256:` digests or MCPB `fileSha256` values.
|
|
82
|
+
|
|
83
|
+
## Discovery Registries
|
|
84
|
+
|
|
85
|
+
Broad directories and scraped indexes should start as discovery sources:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"registries": [
|
|
90
|
+
{
|
|
91
|
+
"id": "glama-public",
|
|
92
|
+
"type": "http-json",
|
|
93
|
+
"url": "https://example.com/mcp-servers.json",
|
|
94
|
+
"mode": "discovery"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Discovery entries can appear in search and info views when their source is enabled. ToolPin refuses to install them until they normalize into a source that is explicitly marked `installable`, or an adapter can safely re-resolve them to an installable registry entry.
|
|
101
|
+
|
|
102
|
+
This is why a source such as Glama can be useful for discovery while still not
|
|
103
|
+
providing installable metadata. It may describe many servers, gateways, or
|
|
104
|
+
directory matches, but ToolPin still needs a matching official entry with a
|
|
105
|
+
lockable package or remote target before it can review, install, and enforce the
|
|
106
|
+
entry.
|
|
107
|
+
|
|
108
|
+
This keeps the product claim precise: ToolPin can search broad directories, but only installs servers it can normalize, review, lock, and enforce.
|
|
109
|
+
|
|
110
|
+
## Config Reference
|
|
111
|
+
|
|
112
|
+
Each entry in `.toolpin/registries.json` supports:
|
|
113
|
+
|
|
114
|
+
| Field | Required | Default | Notes |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| `id` | yes | — | Stable source identifier used by `--source`. |
|
|
117
|
+
| `url` | yes | — | Registry endpoint. For `official-compatible`, ToolPin appends `/servers`. |
|
|
118
|
+
| `type` | no | `official-compatible` | `official-compatible` (MCP Registry `/v0/servers` shape) or `http-json` (response with a `servers` or `entries` array). |
|
|
119
|
+
| `mode` | no | `installable` for `official-compatible`, `discovery` for `http-json` | Whether entries from this source can be installed. |
|
|
120
|
+
| `label` | no | the `id` | Display label in `toolpin registry list` and the TUI. |
|
|
121
|
+
| `trust` | no | `private` | One of `canonical`, `curated`, `directory`, `private`. |
|
|
122
|
+
| `enabled` | no | `true` | For custom registry entries, set to `false` to hide the source from `--source all`; optional built-ins are toggled through top-level `sources` preferences or `toolpin registry enable/disable`. Pinned sources such as `toolpin` cannot be disabled. |
|
|
123
|
+
| `authEnv` | no | — | Environment variable name that holds an auth token; marks the source `authRequired`. Advisory only — ToolPin does not yet send this token in registry requests. |
|
|
124
|
+
| `description` | no | generated | Human-readable source description. |
|
|
125
|
+
|
|
126
|
+
Invalid `type` or `mode` values are rejected. If a custom entry reuses a built-in `id`, the built-in wins and the custom entry is ignored.
|
|
127
|
+
|
|
128
|
+
## Source Resolution and Caching
|
|
129
|
+
|
|
130
|
+
`--source all` fetches every enabled source in parallel and deduplicates entries by repository URL, server name, and version. On collisions, `toolpin` beats `official`, which beats `docker`, which beats any custom source. Disabled sources can remain in the cache, but browse/search filter them out until they are enabled again.
|
|
131
|
+
|
|
132
|
+
The TUI Browse list uses the same source priority by default: `toolpin`,
|
|
133
|
+
`official`, `docker`, then other enabled sources. Press `a` to cycle sort modes
|
|
134
|
+
or `g` to cycle the exact source filter.
|
|
135
|
+
|
|
136
|
+
`toolpin ingest` always fetches live and writes the combined result to `.toolpin/registry-cache.json` (a `{ generatedAt, entries }` document). Other commands read that cache when `--live` is omitted; if the cache is missing or does not contain the requested source, they transparently fall back to a live fetch. Pass `--live` to bypass the cache entirely. A cache file that exists but is not valid registry-cache JSON raises a `CacheSchemaError` instead of falling back.
|
|
137
|
+
|
|
138
|
+
## TUI Installed View
|
|
139
|
+
|
|
140
|
+
Run:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
toolpin tui
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Open the `Installed` tab to inspect servers already written to supported config files across folder/project and global/user scopes. The view shows:
|
|
147
|
+
|
|
148
|
+
- config file, client, and scope
|
|
149
|
+
- locked, unlocked, or drift state
|
|
150
|
+
- registry match status: `registry:exact`, `registry:alias`, or `registry:none`
|
|
151
|
+
- locked and latest known versions
|
|
152
|
+
- lifecycle action: `action:update`, `action:adopt`, or `action:none`
|
|
153
|
+
- test source: `test:config` when ToolPin can test the installed client config
|
|
154
|
+
- delete, update/adopt, explicit version relock, doctor, and `test-installed` actions
|
|
155
|
+
|
|
156
|
+
Runtime status is advisory. ToolPin can mark a server `reachable` after `t` succeeds and `stale` when lock/config or version drift is detected. It does not claim process monitoring unless ToolPin owns the runtime or can query a client/gateway API.
|