agentfences 0.6.0 → 0.7.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 +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp.d.ts +40 -0
- package/dist/mcp.js +46 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.js +24 -19
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,16 @@ const deploy = tool(deployImpl, { guard: true, timeoutS: 30 });
|
|
|
107
107
|
|
|
108
108
|
A guarded or timed tool always returns a promise. What each part guarantees: [ENFORCEMENT.md](../ENFORCEMENT.md).
|
|
109
109
|
|
|
110
|
+
**MCP tools** (from 0.7.0): wrap an MCP `Client` and every `callTool` is recorded, with no `tool()` per tool. The tools you name are guarded as above:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { mcp } from "agentfences";
|
|
114
|
+
|
|
115
|
+
mcp.wrap(client, { guard: ["send_email"], costUsd: { refund: (args) => args.amount as number } });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`guard: true` guards every tool, and `approve: [...]` names tools a person approves first. A result with `isError` is recorded as a failed call and still comes back to the agent.
|
|
119
|
+
|
|
110
120
|
## Not yet in the TypeScript SDK
|
|
111
121
|
|
|
112
122
|
The Python SDK also records OpenAI/Anthropic calls automatically (`instrument`) and integrates with LangGraph and the OpenAI Agents SDK. Those are also how it checks a model call against the budget **before** it's sent, so the TypeScript SDK doesn't do that yet: a model call is checked afterwards, by `checkpoint()`, and a run can go over its budget by one call. Everything here follows the same server protocol ([PROTOCOL.md](../PROTOCOL.md)).
|
package/dist/index.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export { priceOf } from "./pricing.js";
|
|
|
10
10
|
export type { CustomPrice } from "./pricing.js";
|
|
11
11
|
export { FencesError, FencesAuthError, AgentQuarantined, FencesStop, ToolBlocked } from "./errors.js";
|
|
12
12
|
export { VERSION } from "./version.js";
|
|
13
|
+
export * as mcp from "./mcp.js";
|
package/dist/index.js
CHANGED
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
type Args = Record<string, unknown>;
|
|
2
|
+
interface CallToolClient {
|
|
3
|
+
callTool(params: {
|
|
4
|
+
name: string;
|
|
5
|
+
arguments?: Args;
|
|
6
|
+
}, ...rest: any[]): Promise<any>;
|
|
7
|
+
}
|
|
8
|
+
export interface WrapOptions {
|
|
9
|
+
/** Tool names to check with Fences before each call, or true for every tool. */
|
|
10
|
+
guard?: boolean | string[];
|
|
11
|
+
/** Per tool, what a call costs the run: a number or a function of its arguments. Such a tool is guarded. */
|
|
12
|
+
costUsd?: Record<string, number | ((args: Args) => number)>;
|
|
13
|
+
/** Tool names a person approves from the dashboard first. Such a tool is guarded. */
|
|
14
|
+
approve?: string[];
|
|
15
|
+
}
|
|
16
|
+
/** An MCP tool answered with isError: recorded as the call's error, the result still goes back. */
|
|
17
|
+
export declare class MCPToolError extends Error {
|
|
18
|
+
readonly result: {
|
|
19
|
+
content?: {
|
|
20
|
+
text?: string;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
constructor(result: {
|
|
24
|
+
content?: {
|
|
25
|
+
text?: string;
|
|
26
|
+
}[];
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Record every tool call an MCP client makes, and guard the ones you name:
|
|
31
|
+
*
|
|
32
|
+
* mcp.wrap(client, { guard: ["refund", "send_email"], costUsd: { refund: (a) => a.amount as number } });
|
|
33
|
+
*
|
|
34
|
+
* Patches client.callTool in place and returns the client. Each call inside a governed run
|
|
35
|
+
* becomes a tool span, with no tool() per tool. A result with isError counts as a failed call.
|
|
36
|
+
* Guarded tools go through the same check and settle as tool(fn, { guard: true }); a refused
|
|
37
|
+
* one throws ToolBlocked and never reaches the MCP server. Outside a governed run calls pass straight through.
|
|
38
|
+
*/
|
|
39
|
+
export declare function wrap<C extends CallToolClient>(client: C, options?: WrapOptions): C;
|
|
40
|
+
export {};
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getActiveRun } from "./core.js";
|
|
2
|
+
import { argsSummary, callAsync } from "./tools.js";
|
|
3
|
+
/** An MCP tool answered with isError: recorded as the call's error, the result still goes back. */
|
|
4
|
+
export class MCPToolError extends Error {
|
|
5
|
+
result;
|
|
6
|
+
constructor(result) {
|
|
7
|
+
super((result.content ?? []).map((c) => c.text ?? "").join(" ").trim() || "isError");
|
|
8
|
+
this.result = result;
|
|
9
|
+
this.name = "MCPToolError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Record every tool call an MCP client makes, and guard the ones you name:
|
|
14
|
+
*
|
|
15
|
+
* mcp.wrap(client, { guard: ["refund", "send_email"], costUsd: { refund: (a) => a.amount as number } });
|
|
16
|
+
*
|
|
17
|
+
* Patches client.callTool in place and returns the client. Each call inside a governed run
|
|
18
|
+
* becomes a tool span, with no tool() per tool. A result with isError counts as a failed call.
|
|
19
|
+
* Guarded tools go through the same check and settle as tool(fn, { guard: true }); a refused
|
|
20
|
+
* one throws ToolBlocked and never reaches the MCP server. Outside a governed run calls pass straight through.
|
|
21
|
+
*/
|
|
22
|
+
export function wrap(client, options = {}) {
|
|
23
|
+
const original = client.callTool.bind(client), costs = options.costUsd ?? {}, approve = new Set(options.approve);
|
|
24
|
+
const named = new Set(Array.isArray(options.guard) ? options.guard : []);
|
|
25
|
+
client.callTool = async (params, ...rest) => {
|
|
26
|
+
const run = getActiveRun();
|
|
27
|
+
if (!run)
|
|
28
|
+
return original(params, ...rest);
|
|
29
|
+
const { name } = params, args = params.arguments ?? {}, c = Object.hasOwn(costs, name) ? costs[name] : undefined;
|
|
30
|
+
const guarded = options.guard === true || named.has(name) || c !== undefined || approve.has(name);
|
|
31
|
+
try {
|
|
32
|
+
return await callAsync(run, name, argsSummary([args]), guarded ? () => Number((typeof c === "function" ? c(args) : c) ?? 0) : null, approve.has(name), async () => {
|
|
33
|
+
const result = await original(params, ...rest);
|
|
34
|
+
if (result?.isError)
|
|
35
|
+
throw new MCPToolError(result);
|
|
36
|
+
return result;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
if (e instanceof MCPToolError)
|
|
41
|
+
return e.result;
|
|
42
|
+
throw e;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return client;
|
|
46
|
+
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { type Run } from "./core.js";
|
|
1
2
|
/** `{"query":"cats","limit":5}`: the arguments as compact JSON (one object argument on its own), capped. */
|
|
2
3
|
export declare function argsSummary(args: unknown[]): string;
|
|
4
|
+
/** Record an async tool call; cost is null for an unguarded call, else a function giving its amount. */
|
|
5
|
+
export declare function callAsync<R>(run: Run, name: string, summary: string, cost: (() => number) | null, needsApproval: boolean, call: () => Promise<R>): Promise<R>;
|
|
3
6
|
interface ToolFields<A extends unknown[]> {
|
|
4
7
|
name?: string;
|
|
5
8
|
/** Reject with a TimeoutError if the call takes longer. JS can't stop the tool's code; pass run.signal to its fetches. */
|
package/dist/tools.js
CHANGED
|
@@ -108,6 +108,28 @@ async function settle(run, guardId, amount, error, failed) {
|
|
|
108
108
|
run.costUsd += charged;
|
|
109
109
|
await _client()?.guard(run.runId, { guard_id: guardId, phase: "settle", outcome, cost_usd: charged });
|
|
110
110
|
}
|
|
111
|
+
/** Record an async tool call; cost is null for an unguarded call, else a function giving its amount. */
|
|
112
|
+
export async function callAsync(run, name, summary, cost, needsApproval, call) {
|
|
113
|
+
const started = performance.now();
|
|
114
|
+
let gate = null;
|
|
115
|
+
try {
|
|
116
|
+
if (cost) {
|
|
117
|
+
const amount = cost();
|
|
118
|
+
gate = [await open(run, name, summary, amount, needsApproval), amount];
|
|
119
|
+
}
|
|
120
|
+
const out = await call();
|
|
121
|
+
recordToolCall(name, summary, started);
|
|
122
|
+
if (gate)
|
|
123
|
+
await settle(run, ...gate, undefined, false);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
recordToolCall(name, summary, started, e);
|
|
128
|
+
if (gate)
|
|
129
|
+
await settle(run, ...gate, e, true);
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
111
133
|
function timed(p, timeoutS, name) {
|
|
112
134
|
if (timeoutS === undefined)
|
|
113
135
|
return p;
|
|
@@ -125,25 +147,8 @@ export function tool(fn, options = {}) {
|
|
|
125
147
|
const run = getActiveRun();
|
|
126
148
|
if (!run)
|
|
127
149
|
return timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name);
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
if (guarded) {
|
|
132
|
-
const c = options.costUsd, amount = Number((typeof c === "function" ? c(...args) : c) ?? 0);
|
|
133
|
-
gate = [await open(run, name, summary, amount, !!options.approve), amount];
|
|
134
|
-
}
|
|
135
|
-
const out = await timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name);
|
|
136
|
-
recordToolCall(name, summary, started);
|
|
137
|
-
if (gate)
|
|
138
|
-
await settle(run, ...gate, undefined, false);
|
|
139
|
-
return out;
|
|
140
|
-
}
|
|
141
|
-
catch (e) {
|
|
142
|
-
recordToolCall(name, summary, started, e);
|
|
143
|
-
if (gate)
|
|
144
|
-
await settle(run, ...gate, e, true);
|
|
145
|
-
throw e;
|
|
146
|
-
}
|
|
150
|
+
const c = options.costUsd;
|
|
151
|
+
return callAsync(run, name, argsSummary(args), guarded ? () => Number((typeof c === "function" ? c(...args) : c) ?? 0) : null, !!options.approve, () => timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name));
|
|
147
152
|
};
|
|
148
153
|
}
|
|
149
154
|
return (...args) => {
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
|
|
2
|
-
export declare const VERSION = "0.
|
|
2
|
+
export declare const VERSION = "0.7.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.7.0";
|
package/package.json
CHANGED