@webdecoy/ai-protection 0.1.0-alpha.10 → 0.1.0-alpha.11
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/MCP.md +52 -1
- package/action-runtime.mjs +1 -1
- package/actions.d.mts +1 -1
- package/actions.mjs +2 -2
- package/mcp.d.mts +4 -0
- package/mcp.mjs +19 -3
- package/package.json +1 -1
package/MCP.md
CHANGED
|
@@ -11,7 +11,7 @@ resource authorization.
|
|
|
11
11
|
Available in `0.1.0-alpha.5` and later compatible alpha releases:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npm install @webdecoy/ai-protection@0.1.0-alpha.
|
|
14
|
+
npm install @webdecoy/ai-protection@0.1.0-alpha.11 @modelcontextprotocol/sdk@1.31.0
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Requires Node 22.22.3+ and MCP SDK **1.31.0**. The MCP SDK is an optional peer, so
|
|
@@ -269,3 +269,54 @@ can include both sides of a rolling deployment; it does not certify the latest
|
|
|
269
269
|
running configuration or prove that unwrapped routes are protected. A changed
|
|
270
270
|
scope or callback does not change the input-schema hash. No enforcement behavior
|
|
271
271
|
changes when discovery is enabled.
|
|
272
|
+
|
|
273
|
+
## Explicit decoy tools (Alpha)
|
|
274
|
+
|
|
275
|
+
Decoys are off by default. Node alpha.11 supports up to eight explicitly named
|
|
276
|
+
synthetic tools in the protected registry; requires `discovery` and `sharedRuntime`.
|
|
277
|
+
The combined real/decoy registry still has a 128-tool limit. Deploy a compatible
|
|
278
|
+
runtime before upgrading an integration that enables decoys.
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
discovery: {serverId: 'billing'},
|
|
282
|
+
decoys: {
|
|
283
|
+
billing_export_ledger: {
|
|
284
|
+
description: 'Internal ledger export',
|
|
285
|
+
visibility: 'advertised',
|
|
286
|
+
},
|
|
287
|
+
admin_rotate_keys: {
|
|
288
|
+
description: 'Internal key rotation',
|
|
289
|
+
visibility: 'unadvertised',
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Choose names and descriptions outside legitimate workflows; examples are not a
|
|
295
|
+
recommended universal decoy set. Collisions with real tools, callback fields and
|
|
296
|
+
invalid definitions fail startup. Definitions are snapshotted. The SDK provides a
|
|
297
|
+
generic object schema; no custom execution, validation or authorization callbacks
|
|
298
|
+
are accepted for decoys. Advertised decoys appear to authenticated listing clients;
|
|
299
|
+
unadvertised decoys never appear in tools/list. Both accept direct authenticated
|
|
300
|
+
calls only to return the ordinary permission-denied error. Customer code is never
|
|
301
|
+
executed, including when shared telemetry is unavailable. Real tools retain their
|
|
302
|
+
normal authorization and behavior.
|
|
303
|
+
|
|
304
|
+
Only reported calls count as decoy calls; listing is not a trip. Discovery and
|
|
305
|
+
call reports carry a fixed decoy visibility marker, not arguments, hashes of
|
|
306
|
+
arguments, descriptions or generated identities. Existing opt-in caller attribution
|
|
307
|
+
can associate calls with application-reported pseudonyms. Invalid unauthenticated
|
|
308
|
+
requests, over-limit transport bodies and unwrapped routes are not covered by this
|
|
309
|
+
trip evidence. Delivery remains best effort. A marker is SDK-reported evidence,
|
|
310
|
+
not independently verified server configuration.
|
|
311
|
+
|
|
312
|
+
The dashboard labels decoys and mixed real/decoy evidence, counts deduplicated
|
|
313
|
+
calls, and excludes synthetic entries from normal permission/advertisement review
|
|
314
|
+
findings. Name-aggregated activity is labeled when it includes decoys. Reusing a
|
|
315
|
+
name for a real tool can yield mixed evidence until retained reports expire.
|
|
316
|
+
|
|
317
|
+
A decoy call can come from legitimate exploration or a naive agent; it is not proof
|
|
318
|
+
of malicious intent. This slice does not auto-block callers, measure false-positive
|
|
319
|
+
rates, generate seeded names, record initialization events, remotely deliver decoy
|
|
320
|
+
configuration, or provide a labeled test-trigger flow. Configuration requires an
|
|
321
|
+
application deployment. Those remain separate roadmap work. No low-false-positive
|
|
322
|
+
or caller-containment guarantee is made.
|
package/action-runtime.mjs
CHANGED
|
@@ -39,7 +39,7 @@ export function prepareActionRuntime(options, definitions) {
|
|
|
39
39
|
const payload={schema:2,request_id:event.eventId,timestamp:event.timestamp,decision:event.decision,reason:event.reason,
|
|
40
40
|
degraded:event.checks.some(c=>c.decision==='unavailable'),checks,handler_attempted:event.attempted,
|
|
41
41
|
action:event.decision==='deny'?'denied':event.outcome==='unknown'?'handler_error':'forwarded',
|
|
42
|
-
tool_action:{action_id:event.actionId,name:event.action,policy_version:event.policyVersion,outcome:event.outcome,...(event.toolSchema?{tool_schema:{server_id:event.toolSchema.serverId,hash:event.toolSchema.hash,...(event.toolSchema.effect?{effect:event.toolSchema.effect}:{}),...(event.toolSchema.permissions?{permissions:event.toolSchema.permissions}:{})}}:{}),...(event.caller?{caller:event.caller}:{}),...(event.work?{work:event.work}:{})}};
|
|
42
|
+
tool_action:{action_id:event.actionId,name:event.action,policy_version:event.policyVersion,outcome:event.outcome,...(event.toolSchema?{tool_schema:{server_id:event.toolSchema.serverId,hash:event.toolSchema.hash,...(event.toolSchema.decoy?{decoy:event.toolSchema.decoy}:{}),...(event.toolSchema.effect?{effect:event.toolSchema.effect}:{}),...(event.toolSchema.permissions?{permissions:event.toolSchema.permissions}:{})}}:{}),...(event.caller?{caller:event.caller}:{}),...(event.work?{work:event.work}:{})}};
|
|
43
43
|
const response=await fetch(new URL('/api/v1/sdk/ai-abuse/reports',url),{method:'POST',redirect:'error',signal,
|
|
44
44
|
headers:{Authorization:`Bearer ${c.webdecoyKey}`,'X-WebDecoy-Property-ID':c.propertyId,'Content-Type':'application/json'},body:JSON.stringify(payload)});
|
|
45
45
|
await response.body?.cancel();if(!response.ok)throw Error('Action reporting unavailable');
|
package/actions.d.mts
CHANGED
|
@@ -39,7 +39,7 @@ export interface ToolEffectEvidence {
|
|
|
39
39
|
readonly reason: 'insufficient_signals' | 'annotation_read_only' | 'name_read_only' | 'annotation_mutating' | 'name_mutating' | 'schema_mutating' | 'annotation_destructive' | 'name_destructive' | 'schema_destructive' | 'conflicting_hints';
|
|
40
40
|
}
|
|
41
41
|
export interface ToolPermissionEvidence { readonly schema: 1; readonly required_scopes: number; readonly application_authorization: true; readonly additional_policy: boolean; }
|
|
42
|
-
export interface ToolSchemaEvidence { readonly permissions?: ToolPermissionEvidence; readonly serverId: string; readonly hash: string; readonly effect?: ToolEffectEvidence; }
|
|
42
|
+
export interface ToolSchemaEvidence { readonly decoy?: 'advertised' | 'unadvertised'; readonly permissions?: ToolPermissionEvidence; readonly serverId: string; readonly hash: string; readonly effect?: ToolEffectEvidence; }
|
|
43
43
|
export interface ActionEvent {
|
|
44
44
|
readonly toolSchema?: ToolSchemaEvidence;
|
|
45
45
|
/** Pseudonymous application-authenticated subject, not WebDecoy-verified agent identity. */
|
package/actions.mjs
CHANGED
|
@@ -75,8 +75,8 @@ export function createActionProtection(options) {
|
|
|
75
75
|
(definition.policy !== undefined && typeof definition.policy !== 'function')) throw Error('Invalid action definition');
|
|
76
76
|
const t = definition.toolSchema;
|
|
77
77
|
if (t !== undefined && (!t || typeof t.serverId !== 'string' || !token.test(t.serverId) ||
|
|
78
|
-
typeof t.hash !== 'string' || !/^[a-f0-9]{64}$/.test(t.hash))) throw Error('Invalid tool schema evidence');
|
|
79
|
-
const toolSchema = t === undefined ? undefined : Object.freeze({serverId:t.serverId,hash:t.hash,...(t.effect===undefined?{}:{effect:snapshotToolEffect(t.effect)}),...(t.permissions===undefined?{}:{permissions:snapshotToolPermissions(t.permissions)})});
|
|
78
|
+
(t.decoy!==undefined && !['advertised','unadvertised'].includes(t.decoy)) || typeof t.hash !== 'string' || !/^[a-f0-9]{64}$/.test(t.hash))) throw Error('Invalid tool schema evidence');
|
|
79
|
+
const toolSchema = t === undefined ? undefined : Object.freeze({serverId:t.serverId,hash:t.hash,...(t.decoy?{decoy:t.decoy}:{}),...(t.effect===undefined?{}:{effect:snapshotToolEffect(t.effect)}),...(t.permissions===undefined?{}:{permissions:snapshotToolPermissions(t.permissions)})});
|
|
80
80
|
actions.set(name, Object.freeze({...definition,toolSchema,requiredScopes:Object.freeze([...definition.requiredScopes])}));
|
|
81
81
|
}
|
|
82
82
|
if (!actions.size || actions.size > 128) throw Error('Expected 1–128 actions');
|
package/mcp.d.mts
CHANGED
|
@@ -23,6 +23,10 @@ export interface ProtectedMCPOptions {
|
|
|
23
23
|
policyVersion: string;
|
|
24
24
|
sharedRuntime?: ActionRuntime;
|
|
25
25
|
tools: Record<string, ProtectedTool>;
|
|
26
|
+
/** Opt-in, at most eight named decoys. Requires discovery + sharedRuntime.
|
|
27
|
+
* Never accepts customer execution callbacks. Combined real/decoy registry <=128.
|
|
28
|
+
*/
|
|
29
|
+
decoys?: Record<string, {description: string; visibility: 'advertised' | 'unadvertised'}>;
|
|
26
30
|
/** Opt-in tools/list and tools/call metadata: stable non-secret server label and SHA-256 input-schema hashes.
|
|
27
31
|
* Requires sharedRuntime. Reuse serverId across replicas; separate different servers.
|
|
28
32
|
*/
|
package/mcp.mjs
CHANGED
|
@@ -13,6 +13,22 @@ export function createProtectedMCPHandler(options) {
|
|
|
13
13
|
throw Error('Invalid MCP resource configuration');
|
|
14
14
|
const metadataURL = new URL(metadataPath, resource).href;
|
|
15
15
|
const tools = Object.fromEntries(Object.entries(options.tools).map(([name, t]) => [name, { ...t, toolSchema: undefined, annotations: snapshotToolHints(t.annotations), requiredScopes: [...t.requiredScopes], inputSchema: structuredClone(t.inputSchema) }]));
|
|
16
|
+
const decoys = new Map();
|
|
17
|
+
if (options.decoys !== undefined) {
|
|
18
|
+
if (!options.discovery || !options.sharedRuntime || !options.decoys || typeof options.decoys !== 'object' || Array.isArray(options.decoys)) throw Error('Decoys require discovery and sharedRuntime');
|
|
19
|
+
const entries = Object.entries(options.decoys);
|
|
20
|
+
if (entries.length > 8) throw Error('At most 8 decoys are supported');
|
|
21
|
+
for (const [name, definition] of entries) {
|
|
22
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,95}$/.test(name) || Object.hasOwn(tools, name) || !definition ||
|
|
23
|
+
Object.keys(definition).some(k => !['description', 'visibility'].includes(k)) ||
|
|
24
|
+
typeof definition.description !== 'string' || !definition.description.trim() || definition.description.length > 512 ||
|
|
25
|
+
!['advertised', 'unadvertised'].includes(definition.visibility)) throw Error('Invalid or colliding decoy definition');
|
|
26
|
+
decoys.set(name, definition.visibility);
|
|
27
|
+
tools[name] = { description: definition.description, inputSchema: {type:'object'}, requiredScopes:[],
|
|
28
|
+
validate: () => true, authorize: () => false,
|
|
29
|
+
execute: () => { throw Error('Decoy dispatch invariant violated'); } };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
16
32
|
// Validate the closed registry at startup, not only after a client arrives.
|
|
17
33
|
createActionProtection({ policyVersion: options.policyVersion, authenticate: options.authenticate, actions: tools, sharedRuntime: options.sharedRuntime });
|
|
18
34
|
for (const tool of Object.values(tools))
|
|
@@ -32,7 +48,7 @@ export function createProtectedMCPHandler(options) {
|
|
|
32
48
|
const hashes = options.discovery ? Object.fromEntries(Object.entries(tools).map(([name, tool]) =>
|
|
33
49
|
[name, createHash('sha256').update(canonical(JSON.parse(JSON.stringify(tool.inputSchema)))).digest('hex')])) : {};
|
|
34
50
|
if (options.discovery) for (const [name, tool] of Object.entries(tools))
|
|
35
|
-
tool.toolSchema = Object.freeze({serverId:options.discovery.serverId,hash:hashes[name]
|
|
51
|
+
tool.toolSchema = Object.freeze({serverId:options.discovery.serverId,hash:hashes[name],...(decoys.has(name)?{decoy:decoys.get(name)}:{effect:inferToolEffect(name,tool.inputSchema,tool.annotations),permissions:Object.freeze({schema:1,required_scopes:tool.requiredScopes.length,application_authorization:true,additional_policy:typeof tool.policy==='function'})})});
|
|
36
52
|
const runtime = options.sharedRuntime && { ...options.sharedRuntime };
|
|
37
53
|
const serverId = options.discovery?.serverId;
|
|
38
54
|
const catalogReporter = options.discovery ? createReporter({
|
|
@@ -43,7 +59,7 @@ export function createProtectedMCPHandler(options) {
|
|
|
43
59
|
method: 'POST', redirect: 'error', signal,
|
|
44
60
|
headers: { Authorization: `Bearer ${runtime.webdecoyKey}`, 'X-WebDecoy-Property-ID': runtime.propertyId, 'Content-Type': 'application/json' },
|
|
45
61
|
body: JSON.stringify({ schema: 3, request_id: randomUUID(), timestamp: new Date().toISOString(), action: 'tool_discovery',
|
|
46
|
-
tool_catalog: { server_id: serverId, source: 'tools_list', tools: names.map(name => ({ name, schema_hash: hashes[name], effect: tools[name].toolSchema.effect, permissions: tools[name].toolSchema.permissions })) } })
|
|
62
|
+
tool_catalog: { server_id: serverId, source: 'tools_list', tools: names.map(name => ({ name, schema_hash: hashes[name], effect: tools[name].toolSchema.effect, permissions: tools[name].toolSchema.permissions, ...(decoys.has(name)?{decoy:decoys.get(name)}:{}) })) } })
|
|
47
63
|
});
|
|
48
64
|
await response.body?.cancel();
|
|
49
65
|
if (!response.ok) throw Error('Discovery reporting unavailable');
|
|
@@ -197,7 +213,7 @@ export function createProtectedMCPHandler(options) {
|
|
|
197
213
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: false });
|
|
198
214
|
res.once('close', () => { void server.close().catch(() => { }); });
|
|
199
215
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
200
|
-
const visible = Object.entries(tools).filter(([, tool]) => tool.requiredScopes.every(s => caller.scopes.includes(s)));
|
|
216
|
+
const visible = Object.entries(tools).filter(([name, tool]) => decoys.get(name) !== 'unadvertised' && tool.requiredScopes.every(s => caller.scopes.includes(s)));
|
|
201
217
|
for (let i=0;i<visible.length;i+=64) void catalogReporter?.send(visible.slice(i,i+64).map(([name]) => name));
|
|
202
218
|
return { tools: visible.map(([name, tool]) => ({ name, description: tool.description, inputSchema: tool.inputSchema, ...(tool.annotations ? {annotations:tool.annotations} : {}) })) };
|
|
203
219
|
});
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"check:package": "node scripts/check-package.mjs",
|
|
11
11
|
"test:types": "tsc --strict --noEmit --module nodenext --target es2022 test/types.mts test/mcp-types.mts"
|
|
12
12
|
},
|
|
13
|
-
"version": "0.1.0-alpha.
|
|
13
|
+
"version": "0.1.0-alpha.11",
|
|
14
14
|
"exports": {
|
|
15
15
|
"./fetch": {
|
|
16
16
|
"types": "./fetch.d.mts",
|