@webdecoy/ai-protection 0.1.0-alpha.15 → 0.1.0-alpha.16

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 CHANGED
@@ -372,3 +372,25 @@ the revision; enforcement still works but revision feedback remains unknown.
372
372
  Report delivery is best effort and requires the normal reporting queue/flush.
373
373
  This evidence is not acknowledgment from every replica, provider revocation, or
374
374
  independent proof of execution. Outage behavior remains fail open.
375
+
376
+ ### Per-tool pauses (alpha.16)
377
+
378
+ Enable `sharedRuntime.toolPause: true` after updating the hosted runtime. Tool
379
+ pauses cover the exact property, configured server ID and action/tool name,
380
+ across callers and schema versions. MCP supplies tool metadata automatically;
381
+ plain action integrations must provide `toolSchema` on every action. Renaming a
382
+ server/tool changes this identity; an old pause does not cover the new name.
383
+ Tool-only checks do not require `reportCaller` or send a caller pseudonym.
384
+
385
+ Owners/admins manage a tool pause in AI Protection, with an explicit scope
386
+ review, reason, expiry and audit history. Saved controls remain listed even if
387
+ the tool disappears from recent inventory. Other server/tool names stay active.
388
+ The tool remains discoverable; work already admitted is not cancelled.
389
+
390
+ When both `callerPause` and `toolPause` are enabled, they share one online request
391
+ and both checks are reported. Caller denial takes precedence if both deny.
392
+ `callerPauseTimeoutMs` controls the shared deadline (default 1000 ms). There is
393
+ no retry/cache; unavailable, timed-out or malformed checks fail open with explicit
394
+ evidence. Application permissions still apply before this check. Saving is not
395
+ an acknowledgment from every instance. Exact revision evidence permits the
396
+ dashboard to show an SDK-reported tool denial separately from saved state.
@@ -15,6 +15,8 @@ export function prepareActionRuntime(options, definitions) {
15
15
  typeof config.subjectSecret!=='string'||!config.subjectSecret.isWellFormed()||Buffer.byteLength(config.subjectSecret)<32)throw Error('Invalid action runtime');
16
16
  if(config.reportCaller !== undefined && typeof config.reportCaller !== "boolean")throw Error("Invalid caller reporting option");
17
17
  if(config.callerPause !== undefined && typeof config.callerPause !== 'boolean')throw Error('Invalid caller pause option');
18
+ if(config.toolPause !== undefined && typeof config.toolPause !== 'boolean')throw Error('Invalid tool pause option');
19
+ if(config.toolPause && [...definitions.values()].some(d=>!d.toolSchema))throw Error('Tool pause requires toolSchema on every action');
18
20
  if(config.callerPause && !config.reportCaller)throw Error('Caller pause requires caller reporting');
19
21
  const pauseTimeout=config.callerPauseTimeoutMs??1000;
20
22
  if(!Number.isInteger(pauseTimeout)||pauseTimeout<1||pauseTimeout>10000)throw Error('Invalid caller pause timeout');
@@ -70,7 +72,35 @@ export function prepareActionRuntime(options, definitions) {
70
72
  } catch { signal.throwIfAborted(); }
71
73
  check.durationMs=Math.max(0,performance.now()-started);return {check,denial};
72
74
  }:null;
73
- return {limits,checkCallerPause,callerEvidence:caller=>actionCallerEvidence(c,caller),report:event=>reporter.send(event),flush:()=>reporter.flush()};
75
+ const checkToolPause = c.toolPause ? async (caller,name,toolSchema,signal) => {
76
+ signal.throwIfAborted();const started=performance.now();
77
+ const kinds=c.callerPause?['caller','tool']:['tool'];
78
+ let checks=kinds.map(kind=>({id:kind+'_pause',source:'shared',mode:'enforce',decision:'unavailable',reason:kind+'_pause_unavailable',durationMs:0})),denial;
79
+ try {
80
+ const id=c.callerPause?actionCallerEvidence(c,caller).id:'';
81
+ const response=await fetch(new URL('/api/v1/sdk/ai-abuse/caller-pause',c.webdecoyUrl),{
82
+ method:'POST',redirect:'error',signal:AbortSignal.any([signal,AbortSignal.timeout(pauseTimeout)]),
83
+ headers:{Authorization:`Bearer ${c.webdecoyKey}`,'Content-Type':'application/json','X-WebDecoy-Property-ID':c.propertyId},
84
+ body:JSON.stringify({schema:2,caller:id,server_id:toolSchema.serverId,tool:name})
85
+ });
86
+ if(!response.ok){await response.body?.cancel();throw Error('Tool control unavailable');}
87
+ const value=await readJSON(response,4096);
88
+ if(value.schema!==2||value.property_id!==c.propertyId.toLowerCase()||value.caller!==id||value.server_id!==toolSchema.serverId||value.tool!==name||(!c.callerPause&&value.caller_control!==null))throw Error('Invalid tool control response');
89
+ // Validate the entire combined response before accepting either decision.
90
+ const checked=kinds.map(kind=>{
91
+ const control=value[kind+'_control'];
92
+ if(!control||typeof control.allowed!=='boolean'||control.reason!==kind+(control.allowed?'_allowed':'_paused')||
93
+ (control.control_revision!=null&&!validPropertyID(control.control_revision)))throw Error('Invalid control evidence');
94
+ return {id:kind+'_pause',source:'shared',mode:'enforce',decision:control.allowed?'allow':'deny',reason:control.reason,durationMs:0,
95
+ ...(control.control_revision?{controlRevision:control.control_revision.toLowerCase()}:{})};
96
+ });
97
+ checks=checked;const denied=checks.find(check=>check.decision==='deny');
98
+ if(denied)denial={reason:denied.reason,status:403};
99
+ } catch {signal.throwIfAborted();}
100
+ const durationMs=Math.max(0,performance.now()-started);
101
+ return {checks:checks.map(check=>({...check,durationMs})),denial};
102
+ }:null;
103
+ return {limits,checkCallerPause,checkToolPause,callerEvidence:caller=>actionCallerEvidence(c,caller),report:event=>reporter.send(event),flush:()=>reporter.flush()};
74
104
  }
75
105
 
76
106
  export function actionCallerEvidence(config,caller) {
package/actions.d.mts CHANGED
@@ -71,14 +71,16 @@ export interface ActionRuntime {
71
71
  reportCaller?:boolean;
72
72
  /** Opt-in online caller pause check before work. Requires reportCaller. Fails open on timeout (default 1000ms); no cache, retry or in-flight cancellation. */
73
73
  callerPause?:boolean;
74
- /** Caller-pause RPC deadline, 1–10000ms; default 1000ms. Short deadlines can fail open during cold authentication. */
74
+ /** Opt-in tool pause by property/server/name across callers and schema versions. Requires toolSchema on each action; no caller reporting required. Combines with callerPause in one RPC and fails open on unavailable controls. */
75
+ toolPause?:boolean;
76
+ /** Caller/tool-pause RPC deadline, 1–10000ms; default 1000ms. Short deadlines can fail open during cold authentication. */
75
77
  callerPauseTimeoutMs?:number;
76
78
  webdecoyUrl:string;webdecoyKey:string;propertyId:string;subjectSecret:string;
77
79
  reportingTimeoutMs?:number;maxPendingReports?:number;
78
80
  }
79
81
  export interface ActionDefinition {
80
82
  /** Optional non-secret server label and canonical SHA-256 schema hash. MCP discovery fills this automatically.
81
- * Requires a runtime supporting tool_schema evidence; does not change admission.
83
+ * Requires a runtime supporting tool_schema evidence. With toolPause enabled, serverId and the action name identify the control scope.
82
84
  */
83
85
  toolSchema?: ToolSchemaEvidence;
84
86
  limits?: ActionLimits;
package/actions.mjs CHANGED
@@ -133,7 +133,10 @@ export function createActionProtection(options) {
133
133
  // Shared limits run only after application permission checks. Their own
134
134
  // RPC deadlines are separate from local admission and detector availability.
135
135
  clearTimeout(timer);
136
- if(runtime?.checkCallerPause){
136
+ if(runtime?.checkToolPause){
137
+ const r=await runtime.checkToolPause(caller,name,action.toolSchema,admissionSignal);checks.push(...r.checks);
138
+ if(r.denial)deny(r.denial.reason,r.denial.status);
139
+ }else if(runtime?.checkCallerPause){
137
140
  const r=await runtime.checkCallerPause(caller,admissionSignal);checks.push(r.check);
138
141
  if(r.denial)deny(r.denial.reason,r.denial.status);
139
142
  }
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "test:types": "tsc --strict --noEmit --module nodenext --target es2022 test/types.mts test/mcp-types.mts",
12
12
  "test:workers": "node --test test/workers/*.test.mjs"
13
13
  },
14
- "version": "0.1.0-alpha.15",
14
+ "version": "0.1.0-alpha.16",
15
15
  "exports": {
16
16
  "./fetch": {
17
17
  "types": "./fetch.d.mts",