@auth51/mcp-proxy 0.1.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 +146 -0
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +32 -0
- package/dist/audit.js.map +1 -0
- package/dist/authority.d.ts +85 -0
- package/dist/authority.js +266 -0
- package/dist/authority.js.map +1 -0
- package/dist/egress.d.ts +74 -0
- package/dist/egress.js +204 -0
- package/dist/egress.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity.d.ts +42 -0
- package/dist/integrity.js +103 -0
- package/dist/integrity.js.map +1 -0
- package/dist/meta.d.ts +20 -0
- package/dist/meta.js +47 -0
- package/dist/meta.js.map +1 -0
- package/dist/policy.d.ts +14 -0
- package/dist/policy.js +139 -0
- package/dist/policy.js.map +1 -0
- package/dist/pop.d.ts +21 -0
- package/dist/pop.js +76 -0
- package/dist/pop.js.map +1 -0
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.js +337 -0
- package/dist/proxy.js.map +1 -0
- package/dist/scopes.d.ts +43 -0
- package/dist/scopes.js +126 -0
- package/dist/scopes.js.map +1 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/examples/auth51.policy.json +41 -0
- package/examples/demo-drive.mjs +88 -0
- package/examples/mock-mcp-server.mjs +95 -0
- package/package.json +58 -0
package/dist/egress.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server→RS egress: the proxy's Hop-B (non-embed parity, DESIGN-proxy-egress-hop-b).
|
|
3
|
+
*
|
|
4
|
+
* The MCP server's outbound RS calls are routed through this reverse-proxy (v1
|
|
5
|
+
* on-ramp: governed RS base URLs point here). For each request it:
|
|
6
|
+
* 1. maps the CONCRETE (method, path) to the registered endpoint TEMPLATE →
|
|
7
|
+
* the exact `a51:rs:*` scope the verifier will require (O6);
|
|
8
|
+
* 2. obtains a Hop-A intent token — the in-flight one the embed put in `_meta`
|
|
9
|
+
* (Case E), or one the proxy mints itself (Case T) — then EXCHANGES it
|
|
10
|
+
* (RFC 8693) for a downstream, DPoP-bound, RS-scoped token;
|
|
11
|
+
* 3. attaches `Authorization: Bearer <rs-token>` (+ DPoP) and forwards.
|
|
12
|
+
* An ungoverned path (no scope match, no catalog) is a transparent passthrough.
|
|
13
|
+
* Fail-open throughout: any hiccup forwards the request unchanged.
|
|
14
|
+
*/
|
|
15
|
+
import * as http from "node:http";
|
|
16
|
+
import { deriveScope, InvalidScope } from "./scopes.js";
|
|
17
|
+
import * as pop from "./pop.js";
|
|
18
|
+
// ── Ingress→egress bridge (single-flight) ───────────────────────────────────
|
|
19
|
+
// The stdio ingress records the current tool call's intent token here (Case E,
|
|
20
|
+
// from `_meta`); the egress reads it for the exchange. One in-flight tool call
|
|
21
|
+
// at a time for a stdio server, so a single slot is correct (concurrent HTTP
|
|
22
|
+
// servers need a real correlator — see the design §4).
|
|
23
|
+
let _inFlightIntent = null;
|
|
24
|
+
export function setInFlightIntent(token) {
|
|
25
|
+
_inFlightIntent = token;
|
|
26
|
+
}
|
|
27
|
+
export function getInFlightIntent() {
|
|
28
|
+
return _inFlightIntent;
|
|
29
|
+
}
|
|
30
|
+
// ── Route matching: concrete path → registered template → scope ─────────────
|
|
31
|
+
function escapeRe(s) {
|
|
32
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
}
|
|
34
|
+
/** Compile a route template (`/repos/{id}`) to an anchored matcher. */
|
|
35
|
+
export function templateToRegex(template) {
|
|
36
|
+
let re = "";
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < template.length) {
|
|
39
|
+
if (template[i] === "{") {
|
|
40
|
+
const end = template.indexOf("}", i);
|
|
41
|
+
if (end === -1) {
|
|
42
|
+
re += escapeRe(template.slice(i));
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
re += "[^/]+"; // a single path segment
|
|
46
|
+
i = end + 1;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const next = template.indexOf("{", i);
|
|
50
|
+
const lit = next === -1 ? template.slice(i) : template.slice(i, next);
|
|
51
|
+
re += escapeRe(lit);
|
|
52
|
+
i = next === -1 ? template.length : next;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return new RegExp("^" + re + "$");
|
|
56
|
+
}
|
|
57
|
+
/** Strip query + trailing slash, collapse `//` — the concrete request path. */
|
|
58
|
+
export function cleanPath(p) {
|
|
59
|
+
const q = p.indexOf("?");
|
|
60
|
+
if (q >= 0)
|
|
61
|
+
p = p.slice(0, q);
|
|
62
|
+
p = "/" + p.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
63
|
+
return p.replace(/\/{2,}/g, "/") || "/";
|
|
64
|
+
}
|
|
65
|
+
function paramCount(template) {
|
|
66
|
+
return (template.match(/\{[^}]+\}/g) || []).length;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The registered endpoint whose (method, template) matches this concrete
|
|
70
|
+
* request, preferring the most specific (fewest params) on a tie — so a static
|
|
71
|
+
* `/files/latest` wins over `/files/{id}`.
|
|
72
|
+
*/
|
|
73
|
+
export function matchEndpoint(method, path, endpoints) {
|
|
74
|
+
const m = method.toUpperCase();
|
|
75
|
+
const clean = cleanPath(path);
|
|
76
|
+
const hits = endpoints
|
|
77
|
+
.filter((e) => e.method.toUpperCase() === m && templateToRegex(e.pathTemplate).test(clean))
|
|
78
|
+
.sort((a, b) => paramCount(a.pathTemplate) - paramCount(b.pathTemplate));
|
|
79
|
+
return hits[0] ?? null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The `a51:rs` scope to request for this request. Prefer the registered
|
|
83
|
+
* endpoint's stored scope (templated routes need the catalog to be right). If
|
|
84
|
+
* no catalog/match, derive from the concrete path — correct for parameterless
|
|
85
|
+
* routes, best-effort otherwise (a `{id}` route can't be recovered from one
|
|
86
|
+
* concrete path). Returns null only for an underivable request (bad method).
|
|
87
|
+
*/
|
|
88
|
+
export function selectScope(method, path, endpoints, rsId) {
|
|
89
|
+
const ep = matchEndpoint(method, path, endpoints);
|
|
90
|
+
if (ep && ep.scope)
|
|
91
|
+
return { scope: ep.scope, matched: true };
|
|
92
|
+
try {
|
|
93
|
+
return { scope: deriveScope(rsId, method, cleanPath(path)), matched: false };
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
if (e instanceof InvalidScope)
|
|
97
|
+
return null;
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Obtain the downstream RS token for a request: select the scope, get a subject
|
|
103
|
+
* intent token (in-flight from `_meta`, Case E; else mint it, Case T), and
|
|
104
|
+
* exchange it (RFC 8693) for a DPoP-bound RS-scoped token. Null ⇒ passthrough.
|
|
105
|
+
*/
|
|
106
|
+
export async function governedExchange(authority, cfg, method, path, endpoints) {
|
|
107
|
+
const sel = selectScope(method, path, endpoints, cfg.rsId);
|
|
108
|
+
if (!sel)
|
|
109
|
+
return null;
|
|
110
|
+
// Case E: the embed's Hop-A token from _meta. Case T: mint one now (weaker,
|
|
111
|
+
// proxy-asserted identity) — same downstream path either way.
|
|
112
|
+
let subject = getInFlightIntent();
|
|
113
|
+
if (!subject) {
|
|
114
|
+
const minted = await authority.mintIntent({ scope: sel.scope });
|
|
115
|
+
subject = minted?.token ?? null;
|
|
116
|
+
}
|
|
117
|
+
if (!subject)
|
|
118
|
+
return null; // fail-open (no token → forward unchanged)
|
|
119
|
+
const cnfJkt = cfg.popEnabled === false ? undefined : pop.jkt();
|
|
120
|
+
const token = await authority.exchangeToken({
|
|
121
|
+
subjectToken: subject, audience: cfg.audience, scope: sel.scope, cnfJkt,
|
|
122
|
+
});
|
|
123
|
+
if (!token)
|
|
124
|
+
return null;
|
|
125
|
+
return { token, scope: sel.scope, matched: sel.matched, cnfJkt };
|
|
126
|
+
}
|
|
127
|
+
// ── Reverse-proxy server (v1 on-ramp) ───────────────────────────────────────
|
|
128
|
+
async function readBody(req) {
|
|
129
|
+
const chunks = [];
|
|
130
|
+
for await (const c of req)
|
|
131
|
+
chunks.push(c);
|
|
132
|
+
return Buffer.concat(chunks);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Start the egress reverse-proxy. Point the MCP server's governed RS base URL at
|
|
136
|
+
* `http://127.0.0.1:<port>`; the proxy governs each call and forwards to
|
|
137
|
+
* `cfg.targetBaseUrl`. `onAudit` (optional) receives one record per request.
|
|
138
|
+
*/
|
|
139
|
+
export function createEgressServer(authority, cfg, onAudit) {
|
|
140
|
+
let endpoints = null;
|
|
141
|
+
const getEndpoints = async () => {
|
|
142
|
+
if (endpoints === null)
|
|
143
|
+
endpoints = await authority.fetchEndpoints(cfg.rsId);
|
|
144
|
+
return endpoints;
|
|
145
|
+
};
|
|
146
|
+
const server = http.createServer((req, res) => {
|
|
147
|
+
void (async () => {
|
|
148
|
+
const method = req.method || "GET";
|
|
149
|
+
const path = req.url || "/";
|
|
150
|
+
const body = await readBody(req);
|
|
151
|
+
const targetUrl = cfg.targetBaseUrl.replace(/\/$/, "") + path;
|
|
152
|
+
let result = null;
|
|
153
|
+
try {
|
|
154
|
+
result = await governedExchange(authority, cfg, method, path, await getEndpoints());
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
result = null; // fail-open
|
|
158
|
+
}
|
|
159
|
+
const headers = {};
|
|
160
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
161
|
+
if (typeof v === "string" && k.toLowerCase() !== "host")
|
|
162
|
+
headers[k] = v;
|
|
163
|
+
}
|
|
164
|
+
if (result) {
|
|
165
|
+
headers["authorization"] = `Bearer ${result.token}`;
|
|
166
|
+
if (result.cnfJkt) {
|
|
167
|
+
try {
|
|
168
|
+
headers["dpop"] = pop.proof(method, targetUrl, { token: result.token });
|
|
169
|
+
}
|
|
170
|
+
catch { /* proof best-effort */ }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const upstream = await fetch(targetUrl, {
|
|
175
|
+
method,
|
|
176
|
+
headers,
|
|
177
|
+
body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : body,
|
|
178
|
+
});
|
|
179
|
+
const buf = Buffer.from(await upstream.arrayBuffer());
|
|
180
|
+
res.statusCode = upstream.status;
|
|
181
|
+
upstream.headers.forEach((val, key) => {
|
|
182
|
+
if (key.toLowerCase() !== "content-encoding")
|
|
183
|
+
res.setHeader(key, val);
|
|
184
|
+
});
|
|
185
|
+
res.end(buf);
|
|
186
|
+
onAudit?.({
|
|
187
|
+
method, path, target: targetUrl, status: upstream.status,
|
|
188
|
+
governed: !!result, scope: result?.scope ?? null,
|
|
189
|
+
matched: result?.matched ?? null,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
res.statusCode = 502;
|
|
194
|
+
res.end(JSON.stringify({ error: `egress forward failed: ${err.message}` }));
|
|
195
|
+
}
|
|
196
|
+
})();
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
server,
|
|
200
|
+
port: () => server.address().port,
|
|
201
|
+
close: () => new Promise((resolve) => server.close(() => resolve())),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=egress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.js","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { startProxy } from "./proxy.js";
|
|
4
|
+
const USAGE = `auth51-mcp-proxy — least-privilege enforcement + audit at the MCP tool boundary.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
auth51-mcp-proxy [options] -- <mcp-server-command> [args...]
|
|
8
|
+
|
|
9
|
+
Wrap any MCP server. Put this in front of an entry in your Claude Code / Cursor
|
|
10
|
+
MCP config, e.g.:
|
|
11
|
+
|
|
12
|
+
"filesystem": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@auth51/mcp-proxy", "--",
|
|
15
|
+
"npx", "-y", "@modelcontextprotocol/server-filesystem", "/work"]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--policy <path> Policy JSON file (default: built-in safe defaults)
|
|
20
|
+
--audit <path> Append-only JSONL audit log (default: ./.auth51/audit.jsonl)
|
|
21
|
+
--label <name> Friendly name for this server in logs/audit
|
|
22
|
+
--grant <a,b,c> Static capability grant: comma-separated allowed tool names
|
|
23
|
+
(also AUTH51_GRANT). tools/list is filtered to this set;
|
|
24
|
+
calls outside it are denied. Omit = allow all. When an
|
|
25
|
+
authority + --app-id is configured, the agent's server-side
|
|
26
|
+
grant supersedes this and honours its observe/enforce mode.
|
|
27
|
+
--app-id <name> App id (e.g. "Patchet"), also AUTH51_APP_ID. With an
|
|
28
|
+
authority, the proxy fetches this agent's grant and filters
|
|
29
|
+
the tool surface to it (Slice 2d).
|
|
30
|
+
--agent <id> Agent identity label or checksum to attribute decisions to
|
|
31
|
+
--authority <url> auth51 authority base URL (also via AUTH51_AUTHORITY_URL).
|
|
32
|
+
Requires AUTH51_CLIENT_ID + AUTH51_CLIENT_SECRET.
|
|
33
|
+
-h, --help Show this help
|
|
34
|
+
|
|
35
|
+
Authority env:
|
|
36
|
+
AUTH51_AUTHORITY_URL, AUTH51_CLIENT_ID, AUTH51_CLIENT_SECRET, AUTH51_AUDIENCE,
|
|
37
|
+
AUTH51_AGENT_ID, AUTH51_AGENT_CHECKSUM, AUTH51_APP_ID (the registered agent to mint intent tokens / fetch the grant for)
|
|
38
|
+
|
|
39
|
+
Everything works with NO account: local policy enforces, decisions are logged
|
|
40
|
+
locally. Connect an authority to back allowed calls with scoped intent tokens
|
|
41
|
+
and centralize the audit trail for a team / enterprise tenant.
|
|
42
|
+
`;
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const sep = argv.indexOf("--");
|
|
45
|
+
if (sep === -1) {
|
|
46
|
+
process.stderr.write(USAGE);
|
|
47
|
+
process.stderr.write(`\nerror: missing "--" followed by the MCP server command.\n`);
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
const flags = argv.slice(0, sep);
|
|
51
|
+
const rest = argv.slice(sep + 1);
|
|
52
|
+
if (flags.includes("-h") || flags.includes("--help")) {
|
|
53
|
+
process.stdout.write(USAGE);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
if (rest.length === 0) {
|
|
57
|
+
process.stderr.write(`error: no MCP server command after "--".\n`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
const getFlag = (name) => {
|
|
61
|
+
const i = flags.indexOf(name);
|
|
62
|
+
return i >= 0 && i + 1 < flags.length ? flags[i + 1] : undefined;
|
|
63
|
+
};
|
|
64
|
+
const command = rest[0];
|
|
65
|
+
const commandArgs = rest.slice(1);
|
|
66
|
+
const serverLabel = getFlag("--label") ?? basename(command);
|
|
67
|
+
const grantRaw = getFlag("--grant") ?? process.env.AUTH51_GRANT;
|
|
68
|
+
const grant = grantRaw
|
|
69
|
+
? grantRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
70
|
+
: undefined;
|
|
71
|
+
const authorityUrl = getFlag("--authority") ?? process.env.AUTH51_AUTHORITY_URL;
|
|
72
|
+
let authority;
|
|
73
|
+
if (authorityUrl) {
|
|
74
|
+
const clientId = process.env.AUTH51_CLIENT_ID;
|
|
75
|
+
const clientSecret = process.env.AUTH51_CLIENT_SECRET;
|
|
76
|
+
if (!clientId || !clientSecret) {
|
|
77
|
+
process.stderr.write(`[auth51] --authority set but AUTH51_CLIENT_ID / AUTH51_CLIENT_SECRET missing; ` +
|
|
78
|
+
`running local-only.\n`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
authority = {
|
|
82
|
+
baseUrl: authorityUrl,
|
|
83
|
+
clientId,
|
|
84
|
+
clientSecret,
|
|
85
|
+
audience: process.env.AUTH51_AUDIENCE,
|
|
86
|
+
agentId: process.env.AUTH51_AGENT_ID,
|
|
87
|
+
agentChecksum: process.env.AUTH51_AGENT_CHECKSUM,
|
|
88
|
+
appId: getFlag("--app-id") ?? process.env.AUTH51_APP_ID,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Server→RS egress governance (Hop-B). Opt-in: only when a target RS base URL
|
|
93
|
+
// is given. rsId/audience default to AUTH51_AUDIENCE (the governed RS).
|
|
94
|
+
let egress;
|
|
95
|
+
const egressTarget = getFlag("--egress-target") ?? process.env.AUTH51_EGRESS_TARGET;
|
|
96
|
+
if (egressTarget) {
|
|
97
|
+
const rsId = getFlag("--rs-id") ?? process.env.AUTH51_RS_ID ?? process.env.AUTH51_AUDIENCE;
|
|
98
|
+
if (!rsId) {
|
|
99
|
+
process.stderr.write("[auth51] --egress-target set but no --rs-id / AUTH51_RS_ID / AUTH51_AUDIENCE; skipping egress.\n");
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const portStr = getFlag("--egress-port") ?? process.env.AUTH51_EGRESS_PORT;
|
|
103
|
+
egress = {
|
|
104
|
+
rsId,
|
|
105
|
+
audience: process.env.AUTH51_AUDIENCE ?? rsId,
|
|
106
|
+
targetBaseUrl: egressTarget,
|
|
107
|
+
port: portStr ? Number(portStr) : undefined,
|
|
108
|
+
popEnabled: process.env.AUTH51_POP_ENABLED !== "0",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
command,
|
|
114
|
+
commandArgs,
|
|
115
|
+
policyPath: getFlag("--policy"),
|
|
116
|
+
auditPath: getFlag("--audit") ?? `${process.cwd()}/.auth51/audit.jsonl`,
|
|
117
|
+
serverLabel,
|
|
118
|
+
grant,
|
|
119
|
+
agent: getFlag("--agent") ?? process.env.AUTH51_AGENT,
|
|
120
|
+
authority,
|
|
121
|
+
egress,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
startProxy(parseArgs(process.argv.slice(2)));
|
|
125
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxC,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCb,CAAC;AAEF,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;QACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACpF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;IAEjC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC5B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;QACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,IAAY,EAAsB,EAAE;QACnD,MAAM,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IAChE,MAAM,KAAK,GAAG,QAAQ;QACpB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;QAC1D,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAChF,IAAI,SAAsC,CAAC;IAC3C,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAC9C,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;QACtD,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAY,EAAE,CAAC;YAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gFAAgF;gBAC9E,uBAAuB,CAC1B,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,SAAS,GAAG;gBACV,OAAO,EAAE,YAAY;gBACrB,QAAQ;gBACR,YAAY;gBACZ,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe;gBACrC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe;gBACpC,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB;gBAChD,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa;aACxD,CAAC;QACJ,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,wEAAwE;IACxE,IAAI,MAA8B,CAAC;IACnC,MAAM,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IACpF,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;QAC3F,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kGAAkG,CACnG,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;YAC3E,MAAM,GAAG;gBACP,IAAI;gBACJ,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI;gBAC7C,aAAa,EAAE,YAAY;gBAC3B,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;gBAC3C,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,KAAK,GAAG;aACnD,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO;QACP,WAAW;QACX,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,sBAAsB;QACvE,WAAW;QACX,KAAK;QACL,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY;QACrD,SAAS;QACT,MAAM;KACP,CAAC;AACJ,CAAC;AAED,UAAU,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** The security-relevant surface of a tool, as seen in a tools/list result. */
|
|
2
|
+
export interface ToolDescriptor {
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
inputSchema?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface DriftFinding {
|
|
8
|
+
tool: string;
|
|
9
|
+
kind: "new" | "changed" | "removed";
|
|
10
|
+
oldFp?: string;
|
|
11
|
+
newFp?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* L0 — metadata fingerprint. Hash of the tool's *declared surface*: name +
|
|
15
|
+
* input schema + description, scoped to the server. Catches description/schema
|
|
16
|
+
* rug-pulls. NOTE (see design §2.2): L0 is blind to "same metadata, changed code"
|
|
17
|
+
* — that needs L1 (artifact) + L3 (behavioral). This is the cheap, necessary layer.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toolFingerprint(serverLabel: string, t: ToolDescriptor): string;
|
|
20
|
+
/**
|
|
21
|
+
* L1 — artifact fingerprint of the launched server (command + args). Any change
|
|
22
|
+
* to a local server's code requires a new artifact *iff* the version is pinned;
|
|
23
|
+
* v0 captures command+args. Richer resolution (lockfile integrity / image digest)
|
|
24
|
+
* is a later enhancement (design §2.2, L2 via the artifactory).
|
|
25
|
+
*/
|
|
26
|
+
export declare function artifactFingerprint(command: string, args: string[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* Tracks tool/artifact fingerprints against a persisted baseline so drift across
|
|
29
|
+
* sessions (and mid-session via tools/list_changed) is detected. In the full
|
|
30
|
+
* model the baseline is the *registered* surface (E1); here it's a local file,
|
|
31
|
+
* which already catches rug-pulls between runs.
|
|
32
|
+
*/
|
|
33
|
+
export declare class IntegrityTracker {
|
|
34
|
+
private path;
|
|
35
|
+
private baseline;
|
|
36
|
+
constructor(path: string);
|
|
37
|
+
private persist;
|
|
38
|
+
/** L1: record/compare the launched-artifact fingerprint. Returns drift if changed. */
|
|
39
|
+
checkArtifact(serverLabel: string, command: string, args: string[]): DriftFinding | null;
|
|
40
|
+
/** L0: record/compare tool metadata fingerprints. Returns drift findings. */
|
|
41
|
+
checkTools(serverLabel: string, tools: ToolDescriptor[]): DriftFinding[];
|
|
42
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
function sha256(s) {
|
|
5
|
+
return createHash("sha256").update(s).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* L0 — metadata fingerprint. Hash of the tool's *declared surface*: name +
|
|
9
|
+
* input schema + description, scoped to the server. Catches description/schema
|
|
10
|
+
* rug-pulls. NOTE (see design §2.2): L0 is blind to "same metadata, changed code"
|
|
11
|
+
* — that needs L1 (artifact) + L3 (behavioral). This is the cheap, necessary layer.
|
|
12
|
+
*/
|
|
13
|
+
export function toolFingerprint(serverLabel, t) {
|
|
14
|
+
const canonical = JSON.stringify({
|
|
15
|
+
s: serverLabel,
|
|
16
|
+
n: t.name,
|
|
17
|
+
d: t.description ?? "",
|
|
18
|
+
i: t.inputSchema ?? null,
|
|
19
|
+
});
|
|
20
|
+
return "l0:" + sha256(canonical);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* L1 — artifact fingerprint of the launched server (command + args). Any change
|
|
24
|
+
* to a local server's code requires a new artifact *iff* the version is pinned;
|
|
25
|
+
* v0 captures command+args. Richer resolution (lockfile integrity / image digest)
|
|
26
|
+
* is a later enhancement (design §2.2, L2 via the artifactory).
|
|
27
|
+
*/
|
|
28
|
+
export function artifactFingerprint(command, args) {
|
|
29
|
+
return "l1:" + sha256(JSON.stringify({ command, args }));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Tracks tool/artifact fingerprints against a persisted baseline so drift across
|
|
33
|
+
* sessions (and mid-session via tools/list_changed) is detected. In the full
|
|
34
|
+
* model the baseline is the *registered* surface (E1); here it's a local file,
|
|
35
|
+
* which already catches rug-pulls between runs.
|
|
36
|
+
*/
|
|
37
|
+
export class IntegrityTracker {
|
|
38
|
+
path;
|
|
39
|
+
baseline = {};
|
|
40
|
+
constructor(path) {
|
|
41
|
+
this.path = path;
|
|
42
|
+
try {
|
|
43
|
+
this.baseline = JSON.parse(readFileSync(path, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* first run — empty baseline */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
persist() {
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
52
|
+
writeFileSync(this.path, JSON.stringify(this.baseline, null, 2), "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* best-effort */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** L1: record/compare the launched-artifact fingerprint. Returns drift if changed. */
|
|
59
|
+
checkArtifact(serverLabel, command, args) {
|
|
60
|
+
const fp = artifactFingerprint(command, args);
|
|
61
|
+
const entry = (this.baseline[serverLabel] ??= { tools: {} });
|
|
62
|
+
let finding = null;
|
|
63
|
+
if (entry.artifact && entry.artifact !== fp) {
|
|
64
|
+
finding = { tool: "(server artifact)", kind: "changed", oldFp: entry.artifact, newFp: fp };
|
|
65
|
+
}
|
|
66
|
+
entry.artifact = fp;
|
|
67
|
+
this.persist();
|
|
68
|
+
return finding;
|
|
69
|
+
}
|
|
70
|
+
/** L0: record/compare tool metadata fingerprints. Returns drift findings. */
|
|
71
|
+
checkTools(serverLabel, tools) {
|
|
72
|
+
const entry = (this.baseline[serverLabel] ??= { tools: {} });
|
|
73
|
+
const hadBaseline = Object.keys(entry.tools).length > 0;
|
|
74
|
+
const findings = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const t of tools) {
|
|
77
|
+
if (!t || typeof t.name !== "string")
|
|
78
|
+
continue;
|
|
79
|
+
const fp = toolFingerprint(serverLabel, t);
|
|
80
|
+
seen.add(t.name);
|
|
81
|
+
const prev = entry.tools[t.name];
|
|
82
|
+
if (prev === undefined) {
|
|
83
|
+
if (hadBaseline)
|
|
84
|
+
findings.push({ tool: t.name, kind: "new", newFp: fp });
|
|
85
|
+
}
|
|
86
|
+
else if (prev !== fp) {
|
|
87
|
+
findings.push({ tool: t.name, kind: "changed", oldFp: prev, newFp: fp });
|
|
88
|
+
}
|
|
89
|
+
entry.tools[t.name] = fp;
|
|
90
|
+
}
|
|
91
|
+
if (hadBaseline) {
|
|
92
|
+
for (const name of Object.keys(entry.tools)) {
|
|
93
|
+
if (!seen.has(name)) {
|
|
94
|
+
findings.push({ tool: name, kind: "removed", oldFp: entry.tools[name] });
|
|
95
|
+
delete entry.tools[name];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.persist();
|
|
100
|
+
return findings;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=integrity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integrity.js","sourceRoot":"","sources":["../src/integrity.ts"],"names":[],"mappings":"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"}
|
package/dist/meta.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP intent-token propagation — the `_meta` profile (Hop A).
|
|
3
|
+
*
|
|
4
|
+
* The minted intent token rides INSIDE the JSON-RPC call under
|
|
5
|
+
* `params._meta["io.auth51/intent"]` (NOT an HTTP header), so it binds to this
|
|
6
|
+
* exact tool + args and travels with the message to the server. This mirrors
|
|
7
|
+
* the embed's shape (auth51.mcp) byte-for-byte — `{ v, token, chain? }` — so a
|
|
8
|
+
* server can't tell whether the embed or this proxy produced it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const INTENT_META_KEY = "io.auth51/intent";
|
|
11
|
+
export declare const META_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Return `line` (a JSON-RPC message) with the intent token injected into
|
|
14
|
+
* `params._meta["io.auth51/intent"]`, preserving any existing `_meta` keys and
|
|
15
|
+
* params. On any parse/shape problem it returns the original line unchanged —
|
|
16
|
+
* injection must never break the call.
|
|
17
|
+
*/
|
|
18
|
+
export declare function injectIntentMeta(line: string, token: string, chain?: Record<string, unknown>): string;
|
|
19
|
+
/** MCP-server side: pull the intent token out of an incoming call's `_meta`. */
|
|
20
|
+
export declare function extractIntent(meta: unknown): string | null;
|
package/dist/meta.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP intent-token propagation — the `_meta` profile (Hop A).
|
|
3
|
+
*
|
|
4
|
+
* The minted intent token rides INSIDE the JSON-RPC call under
|
|
5
|
+
* `params._meta["io.auth51/intent"]` (NOT an HTTP header), so it binds to this
|
|
6
|
+
* exact tool + args and travels with the message to the server. This mirrors
|
|
7
|
+
* the embed's shape (auth51.mcp) byte-for-byte — `{ v, token, chain? }` — so a
|
|
8
|
+
* server can't tell whether the embed or this proxy produced it.
|
|
9
|
+
*/
|
|
10
|
+
export const INTENT_META_KEY = "io.auth51/intent";
|
|
11
|
+
export const META_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Return `line` (a JSON-RPC message) with the intent token injected into
|
|
14
|
+
* `params._meta["io.auth51/intent"]`, preserving any existing `_meta` keys and
|
|
15
|
+
* params. On any parse/shape problem it returns the original line unchanged —
|
|
16
|
+
* injection must never break the call.
|
|
17
|
+
*/
|
|
18
|
+
export function injectIntentMeta(line, token, chain) {
|
|
19
|
+
try {
|
|
20
|
+
const msg = JSON.parse(line);
|
|
21
|
+
if (!msg || typeof msg !== "object")
|
|
22
|
+
return line;
|
|
23
|
+
const params = msg.params && typeof msg.params === "object" ? msg.params : {};
|
|
24
|
+
const meta = params._meta && typeof params._meta === "object" ? params._meta : {};
|
|
25
|
+
const entry = { v: META_VERSION, token };
|
|
26
|
+
if (chain)
|
|
27
|
+
entry.chain = chain;
|
|
28
|
+
meta[INTENT_META_KEY] = entry;
|
|
29
|
+
params._meta = meta;
|
|
30
|
+
msg.params = params;
|
|
31
|
+
return JSON.stringify(msg);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return line;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** MCP-server side: pull the intent token out of an incoming call's `_meta`. */
|
|
38
|
+
export function extractIntent(meta) {
|
|
39
|
+
if (!meta || typeof meta !== "object")
|
|
40
|
+
return null;
|
|
41
|
+
const entry = meta[INTENT_META_KEY];
|
|
42
|
+
if (entry && typeof entry === "object" && typeof entry.token === "string") {
|
|
43
|
+
return entry.token;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=meta.js.map
|
package/dist/meta.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../src/meta.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAClD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,KAAa,EACb,KAA+B;IAE/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAwB,CAAC;QACpD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACjD,MAAM,MAAM,GACV,GAAG,CAAC,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GACR,MAAM,CAAC,KAAK,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,MAAM,KAAK,GAA4B,EAAE,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;QAClE,IAAI,KAAK;YAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;QAC/B,IAAI,CAAC,eAAe,CAAC,GAAG,KAAK,CAAC;QAC9B,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,KAAK,GAAI,IAA4B,CAAC,eAAe,CAAC,CAAC;IAC7D,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC1E,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Policy, ToolCall, Decision } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The default policy if none is supplied. It is deliberately conservative for the
|
|
4
|
+
* coding-agent use case: allow by default, but block the classes of action that
|
|
5
|
+
* caused the real-world incidents (prod data destruction, recursive deletes,
|
|
6
|
+
* credential exfiltration, unscoped privilege escalation).
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_POLICY: Policy;
|
|
9
|
+
export declare class PolicyEngine {
|
|
10
|
+
private policy;
|
|
11
|
+
constructor(policy: Policy);
|
|
12
|
+
static load(path?: string): PolicyEngine;
|
|
13
|
+
evaluate(call: ToolCall): Decision;
|
|
14
|
+
}
|