@warlock.js/ai-tools 4.5.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/cjs/index.cjs +2519 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/contracts/http.type.d.mts +96 -0
- package/esm/contracts/http.type.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +4 -0
- package/esm/contracts/mcp.type.d.mts +216 -0
- package/esm/contracts/mcp.type.d.mts.map +1 -0
- package/esm/contracts/utility.type.d.mts +96 -0
- package/esm/contracts/utility.type.d.mts.map +1 -0
- package/esm/contracts/web.type.d.mts +136 -0
- package/esm/contracts/web.type.d.mts.map +1 -0
- package/esm/errors.d.mts +225 -0
- package/esm/errors.d.mts.map +1 -0
- package/esm/errors.mjs +136 -0
- package/esm/errors.mjs.map +1 -0
- package/esm/http/http-request.d.mts +57 -0
- package/esm/http/http-request.d.mts.map +1 -0
- package/esm/http/http-request.mjs +221 -0
- package/esm/http/http-request.mjs.map +1 -0
- package/esm/index.d.mts +19 -0
- package/esm/index.mjs +15 -0
- package/esm/mcp/client.mjs +199 -0
- package/esm/mcp/client.mjs.map +1 -0
- package/esm/mcp/index.d.mts +43 -0
- package/esm/mcp/index.d.mts.map +1 -0
- package/esm/mcp/index.mjs +19 -0
- package/esm/mcp/index.mjs.map +1 -0
- package/esm/mcp/json-schema-to-standard.d.mts +34 -0
- package/esm/mcp/json-schema-to-standard.d.mts.map +1 -0
- package/esm/mcp/json-schema-to-standard.mjs +147 -0
- package/esm/mcp/json-schema-to-standard.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +46 -0
- package/esm/mcp/serve.d.mts.map +1 -0
- package/esm/mcp/serve.mjs +264 -0
- package/esm/mcp/serve.mjs.map +1 -0
- package/esm/mcp/transport.d.mts +48 -0
- package/esm/mcp/transport.d.mts.map +1 -0
- package/esm/mcp/transport.mjs +381 -0
- package/esm/mcp/transport.mjs.map +1 -0
- package/esm/mcp/transport.type.d.mts +51 -0
- package/esm/mcp/transport.type.d.mts.map +1 -0
- package/esm/node_modules/@standard-schema/spec/dist/index.d.mts +80 -0
- package/esm/node_modules/@standard-schema/spec/dist/index.d.mts.map +1 -0
- package/esm/register.d.mts +55 -0
- package/esm/register.d.mts.map +1 -0
- package/esm/register.mjs +21 -0
- package/esm/register.mjs.map +1 -0
- package/esm/schema.mjs +127 -0
- package/esm/schema.mjs.map +1 -0
- package/esm/utility/calculator.d.mts +35 -0
- package/esm/utility/calculator.d.mts.map +1 -0
- package/esm/utility/calculator.mjs +272 -0
- package/esm/utility/calculator.mjs.map +1 -0
- package/esm/utility/date-time.d.mts +57 -0
- package/esm/utility/date-time.d.mts.map +1 -0
- package/esm/utility/date-time.mjs +193 -0
- package/esm/utility/date-time.mjs.map +1 -0
- package/esm/utility/index.d.mts +2 -0
- package/esm/utility/index.mjs +4 -0
- package/esm/utility/schema.mjs +114 -0
- package/esm/utility/schema.mjs.map +1 -0
- package/esm/web/fetch-url.d.mts +39 -0
- package/esm/web/fetch-url.d.mts.map +1 -0
- package/esm/web/fetch-url.mjs +228 -0
- package/esm/web/fetch-url.mjs.map +1 -0
- package/esm/web/index.d.mts +2 -0
- package/esm/web/index.mjs +4 -0
- package/esm/web/schema.mjs +86 -0
- package/esm/web/schema.mjs.map +1 -0
- package/esm/web/web-search.d.mts +38 -0
- package/esm/web/web-search.d.mts.map +1 -0
- package/esm/web/web-search.mjs +167 -0
- package/esm/web/web-search.mjs.map +1 -0
- package/llms-full.txt +326 -0
- package/llms.txt +11 -0
- package/package.json +45 -0
- package/skills/README.md +17 -0
- package/skills/connect-mcp-server/SKILL.md +98 -0
- package/skills/expose-as-mcp-server/SKILL.md +85 -0
- package/skills/use-web-and-http-tools/SKILL.md +125 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../@warlock.js/ai-tools/src/errors.ts"],"mappings":";;;;;AAUA;;;;AAA6B;AAM7B;KANY,iBAAA;;;;;KAMA,sBAAA,GAAyB,cAAA;EAEZ,kDAAvB,IAAA,EAAM,iBAAiB;AAAA;;;;;;;;;;;;;;;;AAyB4C;AAiBrE;;cArBa,eAAA,SAAwB,OAAA;EAqBV;EAAA,SAnBT,IAAA,EAAM,iBAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,sBAAA;AAAA;;;;;;;AA6BxB;AAmBvB;;KA/BY,eAAA;;;;;KAUA,oBAAA,GAAuB,cAAA;EAqBA,iDAnBjC,IAAA,EAAM,eAAe;AAAA;;;;;;AAuB4C;AAoBnE;;;;AAA+B;AAO/B;;;;;cA/Ba,aAAA,SAAsB,OAAA;EAiC3B;EAAA,SA/BU,IAAA,EAAM,eAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,oBAAA;AAAA;AAqD/C;;;;;;;;;;;;AAAA,KAjCY,mBAAA;;;;;AAuC2D;KAhC3D,wBAAA,GAA2B,cAAA;EA0Db,iDAxDxB,IAAA,EAAM,mBAAmB,EAwDD;EAtDxB,MAAA;AAAA;;;;;;;;AAmEoB;AAsBtB;;;;;;;;;;;cAnEa,iBAAA,SAA0B,OAAA;EAuElB;EAAA,SArEH,IAAA,EAAM,mBAAA;EAqEc;EAAA,SAnEpB,MAAA;cAEG,OAAA,UAAiB,OAAA,EAAS,wBAAA;AAAA;;;;AAoFlB;AAM7B;;;;;;;;AAEyB;AAwBzB;;;;KA1FY,cAAA;;;;;KAWA,mBAAA,GAAsB,cAAA;EAiFV,gDA/EtB,IAAA,EAAM,cAAc;AAAA;;;;AAiF+C;;;;;;;;;;;;;;;;cA3DxD,YAAA,SAAqB,OAAA;;WAEhB,IAAA,EAAM,cAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,mBAAA;AAAA;;;;;;;;;;;;KAmBnC,iBAAA;;;;;KAMA,sBAAA,GAAyB,cAAA;kDAEnC,IAAA,EAAM,iBAAiB;AAAA;;;;;;;;;;;;;;;;;;;;;;cAwBZ,eAAA,SAAwB,OAAA;;WAEnB,IAAA,EAAM,iBAAA;cAEH,OAAA,UAAiB,OAAA,EAAS,sBAAA;AAAA"}
|
package/esm/errors.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { AIError } from "@warlock.js/ai";
|
|
2
|
+
|
|
3
|
+
//#region ../@warlock.js/ai-tools/src/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* The `calculator` tool could not evaluate an expression — it was not
|
|
6
|
+
* valid arithmetic, divided by zero, or overflowed to a non-finite
|
|
7
|
+
* value.
|
|
8
|
+
*
|
|
9
|
+
* **Surface.** Thrown inside the tool handler, where `tool()` wraps it
|
|
10
|
+
* into a `ToolExecutionError` whose message is preserved verbatim and
|
|
11
|
+
* reaches the model as `{ error }` data, so the agent self-corrects
|
|
12
|
+
* rather than crashing. Extends the framework {@link AIError} (category
|
|
13
|
+
* `"tool"` via code `TOOL_EXEC_FAILED`) so it flows through the same
|
|
14
|
+
* typed error contract as every other AI error; branch on `error.type`
|
|
15
|
+
* for the specific failure.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* if (error instanceof CalculatorError && error.type === "divide-by-zero") {
|
|
19
|
+
* // the expression divided by zero — ask the model to revise it
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
var CalculatorError = class extends AIError {
|
|
23
|
+
constructor(message, options) {
|
|
24
|
+
super("TOOL_EXEC_FAILED", message, options);
|
|
25
|
+
this.name = "CalculatorError";
|
|
26
|
+
this.type = options.type;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The `date_time` tool could not complete a call — a required field was
|
|
31
|
+
* missing or malformed, the unit/time zone was unrecognized, or the
|
|
32
|
+
* operation is unsupported.
|
|
33
|
+
*
|
|
34
|
+
* **Surface.** Thrown inside the tool handler, where `tool()` wraps it
|
|
35
|
+
* into a `ToolExecutionError` whose message reaches the model as
|
|
36
|
+
* `{ error }` data so the agent self-corrects. Extends the framework
|
|
37
|
+
* {@link AIError} (category `"tool"` via code `TOOL_EXEC_FAILED`); branch
|
|
38
|
+
* on `error.type` for the specific failure.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* if (error instanceof DateTimeError && error.type === "invalid-unit") {
|
|
42
|
+
* // the model passed an unknown unit — re-prompt with the allowed set
|
|
43
|
+
* }
|
|
44
|
+
*/
|
|
45
|
+
var DateTimeError = class extends AIError {
|
|
46
|
+
constructor(message, options) {
|
|
47
|
+
super("TOOL_EXEC_FAILED", message, options);
|
|
48
|
+
this.name = "DateTimeError";
|
|
49
|
+
this.type = options.type;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The MCP client's transport layer failed — it could not connect, the
|
|
54
|
+
* peer spoke malformed JSON-RPC, a call timed out, or the transport was
|
|
55
|
+
* already closed.
|
|
56
|
+
*
|
|
57
|
+
* **Surface.** Connection / handshake failures surface at
|
|
58
|
+
* agent-construction time (the caller `await`s `client.tools()`). A
|
|
59
|
+
* `tools/call` failure raised mid-run is wrapped by `tool()` into a
|
|
60
|
+
* `ToolExecutionError` and reaches the model as `{ error }` data, so the
|
|
61
|
+
* agent self-corrects rather than crashing. Extends the framework
|
|
62
|
+
* {@link AIError} (category `"tool"`, code `TOOL_EXEC_FAILED`) so it
|
|
63
|
+
* flows through the same typed error contract as every other AI error;
|
|
64
|
+
* branch on `error.type` for the specific failure.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* if (error instanceof McpTransportError && error.type === "timeout") {
|
|
68
|
+
* // the remote call exceeded its deadline — retry or escalate
|
|
69
|
+
* }
|
|
70
|
+
*/
|
|
71
|
+
var McpTransportError = class extends AIError {
|
|
72
|
+
constructor(message, options) {
|
|
73
|
+
super("TOOL_EXEC_FAILED", message, options);
|
|
74
|
+
this.name = "McpTransportError";
|
|
75
|
+
this.type = options.type;
|
|
76
|
+
this.method = options.method;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* A web tool failed — a missing optional peer, an absent API key, a host
|
|
81
|
+
* rejected by the `allowHosts` guardrail, an unparseable URL, or a failed
|
|
82
|
+
* network call.
|
|
83
|
+
*
|
|
84
|
+
* **Surface.** Thrown from inside a tool's `execute`, so the framework's
|
|
85
|
+
* `tool()` wrapper catches it and surfaces it in the returned `{ error }`
|
|
86
|
+
* field (`invoke()` never throws) — the agent reads the failure as data
|
|
87
|
+
* and self-corrects rather than crashing. Extends the framework
|
|
88
|
+
* {@link AIError} (category `"tool"` via code `TOOL_EXEC_FAILED`) so it
|
|
89
|
+
* flows through the same typed error contract as every other AI error;
|
|
90
|
+
* branch on `error.type` for the specific failure.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* const { error } = await fetchTool.invoke({ url: "http://evil.test" });
|
|
94
|
+
* if (error instanceof WebToolError && error.type === "denied-host") {
|
|
95
|
+
* // the host was not in allowHosts — surfaced before any network call
|
|
96
|
+
* }
|
|
97
|
+
*/
|
|
98
|
+
var WebToolError = class extends AIError {
|
|
99
|
+
constructor(message, options) {
|
|
100
|
+
super("TOOL_EXEC_FAILED", message, options);
|
|
101
|
+
this.name = "WebToolError";
|
|
102
|
+
this.type = options.type;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The `http_request` tool refused a call its construction-time policy
|
|
107
|
+
* does not permit — a disallowed method, a host outside the allowlist,
|
|
108
|
+
* or an unparseable URL. The rejection happens *before* any network
|
|
109
|
+
* request, so a guarded tool can never be coaxed into reaching an
|
|
110
|
+
* off-allowlist host (an SSRF guardrail).
|
|
111
|
+
*
|
|
112
|
+
* **Surface.** Thrown from inside the tool's `execute`, so the framework's
|
|
113
|
+
* `tool()` wrapper catches it and surfaces it in the returned `{ error }`
|
|
114
|
+
* field (`invoke()` never throws) — the agent reads the typed failure as
|
|
115
|
+
* data and self-corrects rather than crashing. Extends the framework
|
|
116
|
+
* {@link AIError} (category `"tool"` via code `TOOL_EXEC_FAILED`) so it
|
|
117
|
+
* flows through the same typed error contract as every other AI error;
|
|
118
|
+
* branch on `error.type` for the specific failure.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const { error } = await httpTool.invoke({ url: "https://evil.test" });
|
|
122
|
+
* if (error instanceof HttpPolicyError && error.type === "host-not-allowed") {
|
|
123
|
+
* // the model tried to reach a host outside the configured allowlist
|
|
124
|
+
* }
|
|
125
|
+
*/
|
|
126
|
+
var HttpPolicyError = class extends AIError {
|
|
127
|
+
constructor(message, options) {
|
|
128
|
+
super("TOOL_EXEC_FAILED", message, options);
|
|
129
|
+
this.name = "HttpPolicyError";
|
|
130
|
+
this.type = options.type;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { CalculatorError, DateTimeError, HttpPolicyError, McpTransportError, WebToolError };
|
|
136
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-tools/src/errors.ts"],"sourcesContent":["import { AIError, type AIErrorOptions } from \"@warlock.js/ai\";\n\n/**\n * Why the calculator rejected an expression.\n *\n * - `\"syntax\"` — the expression could not be tokenized or parsed\n * (an unknown character, a misplaced operator, unbalanced parens).\n * - `\"divide-by-zero\"` — evaluation divided (or took a modulo) by zero.\n * - `\"overflow\"` — the computed result was not a finite number.\n */\nexport type CalculatorFailure = \"syntax\" | \"divide-by-zero\" | \"overflow\";\n\n/**\n * Options for {@link CalculatorError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type CalculatorErrorOptions = AIErrorOptions & {\n /** Which class of calculator failure occurred. */\n type: CalculatorFailure;\n};\n\n/**\n * The `calculator` tool could not evaluate an expression — it was not\n * valid arithmetic, divided by zero, or overflowed to a non-finite\n * value.\n *\n * **Surface.** Thrown inside the tool handler, where `tool()` wraps it\n * into a `ToolExecutionError` whose message is preserved verbatim and\n * reaches the model as `{ error }` data, so the agent self-corrects\n * rather than crashing. Extends the framework {@link AIError} (category\n * `\"tool\"` via code `TOOL_EXEC_FAILED`) so it flows through the same\n * typed error contract as every other AI error; branch on `error.type`\n * for the specific failure.\n *\n * @example\n * if (error instanceof CalculatorError && error.type === \"divide-by-zero\") {\n * // the expression divided by zero — ask the model to revise it\n * }\n */\nexport class CalculatorError extends AIError {\n /** Which class of calculator failure occurred. */\n public readonly type: CalculatorFailure;\n\n public constructor(message: string, options: CalculatorErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"CalculatorError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why the date-time tool rejected a call.\n *\n * - `\"invalid-input\"` — a required field for the chosen `op` was\n * missing or malformed (an unparsable ISO instant, a bad `amount`).\n * - `\"invalid-unit\"` — `unit` was not one of the supported units.\n * - `\"invalid-time-zone\"` — the IANA time zone was not recognized.\n * - `\"unsupported-op\"` — the `op` was not one this tool implements.\n */\nexport type DateTimeFailure =\n | \"invalid-input\"\n | \"invalid-unit\"\n | \"invalid-time-zone\"\n | \"unsupported-op\";\n\n/**\n * Options for {@link DateTimeError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type DateTimeErrorOptions = AIErrorOptions & {\n /** Which class of date-time failure occurred. */\n type: DateTimeFailure;\n};\n\n/**\n * The `date_time` tool could not complete a call — a required field was\n * missing or malformed, the unit/time zone was unrecognized, or the\n * operation is unsupported.\n *\n * **Surface.** Thrown inside the tool handler, where `tool()` wraps it\n * into a `ToolExecutionError` whose message reaches the model as\n * `{ error }` data so the agent self-corrects. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`); branch\n * on `error.type` for the specific failure.\n *\n * @example\n * if (error instanceof DateTimeError && error.type === \"invalid-unit\") {\n * // the model passed an unknown unit — re-prompt with the allowed set\n * }\n */\nexport class DateTimeError extends AIError {\n /** Which class of date-time failure occurred. */\n public readonly type: DateTimeFailure;\n\n public constructor(message: string, options: DateTimeErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"DateTimeError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why an MCP transport operation failed.\n *\n * - `\"connect\"` — the transport could not be opened (child process\n * failed to spawn, HTTP endpoint unreachable) or the `initialize`\n * handshake failed.\n * - `\"protocol\"` — a malformed / unexpected JSON-RPC message, a\n * response that matched no in-flight request, or a missing field.\n * - `\"timeout\"` — a request exceeded its per-call deadline.\n * - `\"closed\"` — the transport was used after it was closed, or the\n * peer closed it mid-call.\n */\nexport type McpTransportFailure = \"connect\" | \"protocol\" | \"timeout\" | \"closed\";\n\n/**\n * Options for {@link McpTransportError} — the structured `type`\n * discriminator plus an optional JSON-RPC method name for branchable\n * diagnostics without parsing the message.\n */\nexport type McpTransportErrorOptions = AIErrorOptions & {\n /** Which class of transport failure occurred. */\n type: McpTransportFailure;\n /** The JSON-RPC method in flight when the failure occurred, if any. */\n method?: string;\n};\n\n/**\n * The MCP client's transport layer failed — it could not connect, the\n * peer spoke malformed JSON-RPC, a call timed out, or the transport was\n * already closed.\n *\n * **Surface.** Connection / handshake failures surface at\n * agent-construction time (the caller `await`s `client.tools()`). A\n * `tools/call` failure raised mid-run is wrapped by `tool()` into a\n * `ToolExecutionError` and reaches the model as `{ error }` data, so the\n * agent self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"`, code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * if (error instanceof McpTransportError && error.type === \"timeout\") {\n * // the remote call exceeded its deadline — retry or escalate\n * }\n */\nexport class McpTransportError extends AIError {\n /** Which class of transport failure occurred. */\n public readonly type: McpTransportFailure;\n /** The JSON-RPC method in flight when the failure occurred, if any. */\n public readonly method?: string;\n\n public constructor(message: string, options: McpTransportErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"McpTransportError\";\n this.type = options.type;\n this.method = options.method;\n }\n}\n\n/**\n * Why a web tool (`ai.tools.webSearch` / `ai.tools.fetchUrl`) failed\n * before or during a network call.\n *\n * - `\"missing-peer\"` — an optional peer dependency the chosen mode needs\n * (`@mozilla/readability` + `jsdom` for text/markdown extraction, a\n * search provider SDK) is not installed. The message carries a curated\n * `npm install` string for the developer.\n * - `\"missing-key\"` — no API key was supplied via options or the\n * provider's environment variable.\n * - `\"denied-host\"` — the requested URL's host is not in the configured\n * `allowHosts` allowlist (an SSRF guardrail), rejected before any fetch.\n * - `\"invalid-url\"` — the supplied URL could not be parsed, or used a\n * non-`http(s)` scheme.\n * - `\"request-failed\"` — the network call itself failed (DNS, connection\n * reset, timeout) or the provider returned a non-OK status.\n */\nexport type WebToolFailure =\n | \"missing-peer\"\n | \"missing-key\"\n | \"denied-host\"\n | \"invalid-url\"\n | \"request-failed\";\n\n/**\n * Options for {@link WebToolError} — the structured `type` discriminator\n * so a caller can branch without parsing the message.\n */\nexport type WebToolErrorOptions = AIErrorOptions & {\n /** Which class of web-tool failure occurred. */\n type: WebToolFailure;\n};\n\n/**\n * A web tool failed — a missing optional peer, an absent API key, a host\n * rejected by the `allowHosts` guardrail, an unparseable URL, or a failed\n * network call.\n *\n * **Surface.** Thrown from inside a tool's `execute`, so the framework's\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field (`invoke()` never throws) — the agent reads the failure as data\n * and self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * const { error } = await fetchTool.invoke({ url: \"http://evil.test\" });\n * if (error instanceof WebToolError && error.type === \"denied-host\") {\n * // the host was not in allowHosts — surfaced before any network call\n * }\n */\nexport class WebToolError extends AIError {\n /** Which class of web-tool failure occurred. */\n public readonly type: WebToolFailure;\n\n public constructor(message: string, options: WebToolErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"WebToolError\";\n this.type = options.type;\n }\n}\n\n/**\n * Why an `http_request` call was rejected by its own guardrails, before\n * the network request was ever issued.\n *\n * - `\"method-not-allowed\"` — the model requested an HTTP method that is\n * not on the tool's `allowMethods` allowlist (defaults to `[\"GET\"]`).\n * - `\"host-not-allowed\"` — the resolved request host is not on the\n * tool's `allowHosts` allowlist (an SSRF guardrail).\n * - `\"invalid-url\"` — the supplied URL (or its join with `baseUrl`)\n * could not be parsed into an absolute `http(s)` URL.\n */\nexport type HttpPolicyFailure = \"method-not-allowed\" | \"host-not-allowed\" | \"invalid-url\";\n\n/**\n * Options for {@link HttpPolicyError} — the structured `type`\n * discriminator so a caller can branch without parsing the message.\n */\nexport type HttpPolicyErrorOptions = AIErrorOptions & {\n /** Which class of policy rejection occurred. */\n type: HttpPolicyFailure;\n};\n\n/**\n * The `http_request` tool refused a call its construction-time policy\n * does not permit — a disallowed method, a host outside the allowlist,\n * or an unparseable URL. The rejection happens *before* any network\n * request, so a guarded tool can never be coaxed into reaching an\n * off-allowlist host (an SSRF guardrail).\n *\n * **Surface.** Thrown from inside the tool's `execute`, so the framework's\n * `tool()` wrapper catches it and surfaces it in the returned `{ error }`\n * field (`invoke()` never throws) — the agent reads the typed failure as\n * data and self-corrects rather than crashing. Extends the framework\n * {@link AIError} (category `\"tool\"` via code `TOOL_EXEC_FAILED`) so it\n * flows through the same typed error contract as every other AI error;\n * branch on `error.type` for the specific failure.\n *\n * @example\n * const { error } = await httpTool.invoke({ url: \"https://evil.test\" });\n * if (error instanceof HttpPolicyError && error.type === \"host-not-allowed\") {\n * // the model tried to reach a host outside the configured allowlist\n * }\n */\nexport class HttpPolicyError extends AIError {\n /** Which class of policy rejection occurred. */\n public readonly type: HttpPolicyFailure;\n\n public constructor(message: string, options: HttpPolicyErrorOptions) {\n super(\"TOOL_EXEC_FAILED\", message, options);\n\n this.name = \"HttpPolicyError\";\n this.type = options.type;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,kBAAb,cAAqC,QAAQ;CAI3C,AAAO,YAAY,SAAiB,SAAiC;EACnE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;AA0CA,IAAa,gBAAb,cAAmC,QAAQ;CAIzC,AAAO,YAAY,SAAiB,SAA+B;EACjE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;;;;AA+CA,IAAa,oBAAb,cAAuC,QAAQ;CAM7C,AAAO,YAAY,SAAiB,SAAmC;EACrE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;EACpB,KAAK,SAAS,QAAQ;CACxB;AACF;;;;;;;;;;;;;;;;;;;;AAsDA,IAAa,eAAb,cAAkC,QAAQ;CAIxC,AAAO,YAAY,SAAiB,SAA8B;EAChE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF;;;;;;;;;;;;;;;;;;;;;;AA6CA,IAAa,kBAAb,cAAqC,QAAQ;CAI3C,AAAO,YAAY,SAAiB,SAAiC;EACnE,MAAM,oBAAoB,SAAS,OAAO;EAE1C,KAAK,OAAO;EACZ,KAAK,OAAO,QAAQ;CACtB;AACF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { HttpRequestInput, HttpRequestOptions, HttpRequestResult } from "../contracts/http.type.mjs";
|
|
2
|
+
import { ToolContract } from "@warlock.js/ai";
|
|
3
|
+
|
|
4
|
+
//#region ../@warlock.js/ai-tools/src/http/http-request.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build the agent-facing `http_request` tool — a guarded HTTP/REST client
|
|
7
|
+
* over the global `fetch`. The `options` bound what the model may do; the
|
|
8
|
+
* model supplies the per-call URL / method / headers / body within those
|
|
9
|
+
* rails.
|
|
10
|
+
*
|
|
11
|
+
* **Guardrails (all enforced before the network call).**
|
|
12
|
+
* - **Method allowlist** — `allowMethods` (default `["GET"]`). A method
|
|
13
|
+
* outside the list is rejected with a typed
|
|
14
|
+
* {@link HttpPolicyError} (`type: "method-not-allowed"`).
|
|
15
|
+
* - **Host allowlist** — when `allowHosts` is set, any other host is
|
|
16
|
+
* rejected (`type: "host-not-allowed"`), an SSRF guardrail.
|
|
17
|
+
* - **`baseUrl` join** — when configured, the model passes a path that
|
|
18
|
+
* is resolved against `baseUrl`; otherwise it must pass an absolute
|
|
19
|
+
* `http(s)` URL. An unresolvable URL is rejected
|
|
20
|
+
* (`type: "invalid-url"`).
|
|
21
|
+
*
|
|
22
|
+
* **Request shaping.** Static `options.headers` are merged under the
|
|
23
|
+
* per-call `headers` (the per-call value wins). An object `body` is
|
|
24
|
+
* JSON-serialized with a `content-type: application/json` default; a
|
|
25
|
+
* string `body` is sent verbatim; `body` is dropped for bodyless methods
|
|
26
|
+
* (`GET`). The call is bounded by `timeoutMs` (default `15_000`) via an
|
|
27
|
+
* `AbortController`, also wired to `ctx.signal` for cooperative
|
|
28
|
+
* cancellation.
|
|
29
|
+
*
|
|
30
|
+
* **Response shaping.** Headers are returned with lower-cased keys. The
|
|
31
|
+
* body is read up to `maxBytes` (default `1_000_000`) and JSON-parsed
|
|
32
|
+
* when the response `content-type` is JSON, otherwise returned as text;
|
|
33
|
+
* `truncated` is `true` when the body was cut off at the cap (a truncated
|
|
34
|
+
* JSON body is returned as the raw partial string, since it can no longer
|
|
35
|
+
* be parsed).
|
|
36
|
+
*
|
|
37
|
+
* **Errors flow as data.** Every guardrail rejection and network failure
|
|
38
|
+
* is thrown inside `execute`; the framework's `tool()` wrapper catches it
|
|
39
|
+
* and surfaces it in the returned `{ error }` field, so the agent reads
|
|
40
|
+
* the failure and self-corrects rather than crashing.
|
|
41
|
+
*
|
|
42
|
+
* @param options - Construction-time policy bounding the tool.
|
|
43
|
+
* @returns A {@link ToolContract} the agent can call as `http_request`.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const stripe = httpRequestTool({
|
|
47
|
+
* baseUrl: "https://api.stripe.com",
|
|
48
|
+
* allowHosts: ["api.stripe.com"],
|
|
49
|
+
* allowMethods: ["GET", "POST"],
|
|
50
|
+
* headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
|
|
51
|
+
* });
|
|
52
|
+
* const { data } = await stripe.invoke({ method: "GET", url: "/v1/charges" });
|
|
53
|
+
*/
|
|
54
|
+
declare function httpRequestTool(options?: HttpRequestOptions): ToolContract<HttpRequestInput, HttpRequestResult>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { httpRequestTool };
|
|
57
|
+
//# sourceMappingURL=http-request.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-request.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-tools/src/http/http-request.ts"],"mappings":";;;;;;;AAwMA;;;;;;;;;;;;;;;AAEmD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAFnC,eAAA,CACd,OAAA,GAAS,kBAAA,GACR,YAAA,CAAa,gBAAA,EAAkB,iBAAA"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { HttpPolicyError } from "../errors.mjs";
|
|
2
|
+
import { objectSchema, optionalStringEnumField, optionalStringRecordField, passthroughField, stringField } from "../schema.mjs";
|
|
3
|
+
import { tool } from "@warlock.js/ai";
|
|
4
|
+
|
|
5
|
+
//#region ../@warlock.js/ai-tools/src/http/http-request.ts
|
|
6
|
+
/** Default tool name exposed to the LLM. */
|
|
7
|
+
const DEFAULT_NAME = "http_request";
|
|
8
|
+
/** Default per-request wall-clock timeout, in milliseconds. */
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
10
|
+
/** Default hard cap on response-body bytes before truncation. */
|
|
11
|
+
const DEFAULT_MAX_BYTES = 1e6;
|
|
12
|
+
/** The full set of HTTP methods, in the order they appear in {@link HttpMethod}. */
|
|
13
|
+
const ALL_METHODS = [
|
|
14
|
+
"GET",
|
|
15
|
+
"POST",
|
|
16
|
+
"PUT",
|
|
17
|
+
"PATCH",
|
|
18
|
+
"DELETE"
|
|
19
|
+
];
|
|
20
|
+
/** Methods that conventionally carry no request body — `body` is dropped for these. */
|
|
21
|
+
const BODYLESS_METHODS = new Set(["GET"]);
|
|
22
|
+
/**
|
|
23
|
+
* Standard Schema for {@link HttpRequestInput}. `url` is required;
|
|
24
|
+
* `method` is constrained to the canonical HTTP verb set (further
|
|
25
|
+
* narrowed to the tool's `allowMethods` at runtime); `headers` is an
|
|
26
|
+
* optional string-to-string record; `body` is an opaque passthrough the
|
|
27
|
+
* handler serializes based on its runtime type.
|
|
28
|
+
*/
|
|
29
|
+
const httpRequestInputSchema = objectSchema({
|
|
30
|
+
method: optionalStringEnumField(ALL_METHODS),
|
|
31
|
+
url: stringField(),
|
|
32
|
+
headers: optionalStringRecordField(),
|
|
33
|
+
body: passthroughField()
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the request target. With a `baseUrl` configured the model
|
|
37
|
+
* supplies a path joined against it; otherwise the model's `url` must be
|
|
38
|
+
* an absolute `http(s)` URL. Throws a typed {@link HttpPolicyError} of
|
|
39
|
+
* type `"invalid-url"` when the result cannot be parsed or is not an
|
|
40
|
+
* `http`/`https` URL — surfaced as `{ error }` data, never a crash.
|
|
41
|
+
*/
|
|
42
|
+
function resolveUrl(rawUrl, baseUrl) {
|
|
43
|
+
let resolved;
|
|
44
|
+
try {
|
|
45
|
+
resolved = baseUrl !== void 0 ? new URL(rawUrl, baseUrl) : new URL(rawUrl);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new HttpPolicyError(`http_request could not resolve a valid URL from "${rawUrl}"` + (baseUrl !== void 0 ? ` against base "${baseUrl}".` : "."), { type: "invalid-url" });
|
|
48
|
+
}
|
|
49
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") throw new HttpPolicyError(`http_request only permits http(s) URLs; got "${resolved.protocol}".`, { type: "invalid-url" });
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read a `Response` body, capping at `maxBytes`. Returns the decoded text
|
|
54
|
+
* and whether it was cut off. Streams chunk-by-chunk so an oversized body
|
|
55
|
+
* is abandoned at the cap rather than fully buffered; falls back to
|
|
56
|
+
* `response.text()` (then a post-hoc byte slice) when the body is not a
|
|
57
|
+
* readable stream (e.g. a stubbed `Response` in tests).
|
|
58
|
+
*/
|
|
59
|
+
async function readCappedBody(response, maxBytes) {
|
|
60
|
+
const body = response.body;
|
|
61
|
+
if (!body) return {
|
|
62
|
+
text: "",
|
|
63
|
+
truncated: false
|
|
64
|
+
};
|
|
65
|
+
const decoder = new TextDecoder();
|
|
66
|
+
const reader = body.getReader();
|
|
67
|
+
let received = 0;
|
|
68
|
+
let truncated = false;
|
|
69
|
+
let text = "";
|
|
70
|
+
try {
|
|
71
|
+
for (;;) {
|
|
72
|
+
const { done, value } = await reader.read();
|
|
73
|
+
if (done) break;
|
|
74
|
+
if (!value) continue;
|
|
75
|
+
const remaining = maxBytes - received;
|
|
76
|
+
if (value.byteLength > remaining) {
|
|
77
|
+
text += decoder.decode(value.subarray(0, remaining), { stream: true });
|
|
78
|
+
received = maxBytes;
|
|
79
|
+
truncated = true;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
text += decoder.decode(value, { stream: true });
|
|
83
|
+
received += value.byteLength;
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
await reader.cancel().catch(() => void 0);
|
|
87
|
+
reader.releaseLock();
|
|
88
|
+
}
|
|
89
|
+
text += decoder.decode();
|
|
90
|
+
return {
|
|
91
|
+
text,
|
|
92
|
+
truncated
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Decide whether a response's `content-type` indicates JSON. Matches
|
|
97
|
+
* `application/json` and the `+json` structured-suffix convention
|
|
98
|
+
* (e.g. `application/vnd.api+json`), case-insensitively.
|
|
99
|
+
*/
|
|
100
|
+
function isJsonContentType(contentType) {
|
|
101
|
+
if (!contentType) return false;
|
|
102
|
+
const value = contentType.toLowerCase();
|
|
103
|
+
return value.includes("application/json") || value.includes("+json");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build the agent-facing `http_request` tool — a guarded HTTP/REST client
|
|
107
|
+
* over the global `fetch`. The `options` bound what the model may do; the
|
|
108
|
+
* model supplies the per-call URL / method / headers / body within those
|
|
109
|
+
* rails.
|
|
110
|
+
*
|
|
111
|
+
* **Guardrails (all enforced before the network call).**
|
|
112
|
+
* - **Method allowlist** — `allowMethods` (default `["GET"]`). A method
|
|
113
|
+
* outside the list is rejected with a typed
|
|
114
|
+
* {@link HttpPolicyError} (`type: "method-not-allowed"`).
|
|
115
|
+
* - **Host allowlist** — when `allowHosts` is set, any other host is
|
|
116
|
+
* rejected (`type: "host-not-allowed"`), an SSRF guardrail.
|
|
117
|
+
* - **`baseUrl` join** — when configured, the model passes a path that
|
|
118
|
+
* is resolved against `baseUrl`; otherwise it must pass an absolute
|
|
119
|
+
* `http(s)` URL. An unresolvable URL is rejected
|
|
120
|
+
* (`type: "invalid-url"`).
|
|
121
|
+
*
|
|
122
|
+
* **Request shaping.** Static `options.headers` are merged under the
|
|
123
|
+
* per-call `headers` (the per-call value wins). An object `body` is
|
|
124
|
+
* JSON-serialized with a `content-type: application/json` default; a
|
|
125
|
+
* string `body` is sent verbatim; `body` is dropped for bodyless methods
|
|
126
|
+
* (`GET`). The call is bounded by `timeoutMs` (default `15_000`) via an
|
|
127
|
+
* `AbortController`, also wired to `ctx.signal` for cooperative
|
|
128
|
+
* cancellation.
|
|
129
|
+
*
|
|
130
|
+
* **Response shaping.** Headers are returned with lower-cased keys. The
|
|
131
|
+
* body is read up to `maxBytes` (default `1_000_000`) and JSON-parsed
|
|
132
|
+
* when the response `content-type` is JSON, otherwise returned as text;
|
|
133
|
+
* `truncated` is `true` when the body was cut off at the cap (a truncated
|
|
134
|
+
* JSON body is returned as the raw partial string, since it can no longer
|
|
135
|
+
* be parsed).
|
|
136
|
+
*
|
|
137
|
+
* **Errors flow as data.** Every guardrail rejection and network failure
|
|
138
|
+
* is thrown inside `execute`; the framework's `tool()` wrapper catches it
|
|
139
|
+
* and surfaces it in the returned `{ error }` field, so the agent reads
|
|
140
|
+
* the failure and self-corrects rather than crashing.
|
|
141
|
+
*
|
|
142
|
+
* @param options - Construction-time policy bounding the tool.
|
|
143
|
+
* @returns A {@link ToolContract} the agent can call as `http_request`.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* const stripe = httpRequestTool({
|
|
147
|
+
* baseUrl: "https://api.stripe.com",
|
|
148
|
+
* allowHosts: ["api.stripe.com"],
|
|
149
|
+
* allowMethods: ["GET", "POST"],
|
|
150
|
+
* headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },
|
|
151
|
+
* });
|
|
152
|
+
* const { data } = await stripe.invoke({ method: "GET", url: "/v1/charges" });
|
|
153
|
+
*/
|
|
154
|
+
function httpRequestTool(options = {}) {
|
|
155
|
+
const allowMethods = options.allowMethods ?? ["GET"];
|
|
156
|
+
const allowedMethodSet = new Set(allowMethods);
|
|
157
|
+
const allowHostSet = options.allowHosts ? new Set(options.allowHosts) : void 0;
|
|
158
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
159
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
160
|
+
const staticHeaders = options.headers;
|
|
161
|
+
return tool({
|
|
162
|
+
name: options.name ?? DEFAULT_NAME,
|
|
163
|
+
description: "Issue an HTTP request and return the status, response headers, and parsed body. Allowed methods and hosts are restricted by the tool's configuration; a request outside those rails is rejected before any network call. Pass an object body to send JSON, or a string to send it verbatim. The response body is JSON-parsed when the content-type is JSON, otherwise returned as text, and is capped — `truncated` is true when the body was cut off.",
|
|
164
|
+
action: (input) => `Requesting ${input.method ?? "GET"} ${input.url}`,
|
|
165
|
+
input: httpRequestInputSchema,
|
|
166
|
+
async execute(input, ctx) {
|
|
167
|
+
const method = input.method ?? "GET";
|
|
168
|
+
if (!allowedMethodSet.has(method)) throw new HttpPolicyError(`http_request method "${method}" is not allowed. Permitted methods: ${[...allowedMethodSet].join(", ")}.`, { type: "method-not-allowed" });
|
|
169
|
+
const url = resolveUrl(input.url, options.baseUrl);
|
|
170
|
+
if (allowHostSet && !allowHostSet.has(url.hostname)) throw new HttpPolicyError(`http_request host "${url.hostname}" is not in the allowlist. Permitted hosts: ${[...allowHostSet].join(", ")}.`, { type: "host-not-allowed" });
|
|
171
|
+
const headers = {
|
|
172
|
+
...staticHeaders,
|
|
173
|
+
...input.headers
|
|
174
|
+
};
|
|
175
|
+
let body;
|
|
176
|
+
if (!BODYLESS_METHODS.has(method) && input.body !== void 0) if (typeof input.body === "string") body = input.body;
|
|
177
|
+
else {
|
|
178
|
+
body = JSON.stringify(input.body);
|
|
179
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) headers["content-type"] = "application/json";
|
|
180
|
+
}
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
183
|
+
const onAbort = () => controller.abort();
|
|
184
|
+
if (ctx?.signal) if (ctx.signal.aborted) controller.abort();
|
|
185
|
+
else ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
186
|
+
let response;
|
|
187
|
+
try {
|
|
188
|
+
response = await fetch(url, {
|
|
189
|
+
method,
|
|
190
|
+
headers,
|
|
191
|
+
body,
|
|
192
|
+
signal: controller.signal
|
|
193
|
+
});
|
|
194
|
+
} finally {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
ctx?.signal?.removeEventListener("abort", onAbort);
|
|
197
|
+
}
|
|
198
|
+
const responseHeaders = {};
|
|
199
|
+
response.headers.forEach((value, key) => {
|
|
200
|
+
responseHeaders[key.toLowerCase()] = value;
|
|
201
|
+
});
|
|
202
|
+
const { text, truncated } = await readCappedBody(response, maxBytes);
|
|
203
|
+
let parsedBody = text;
|
|
204
|
+
if (!truncated && isJsonContentType(responseHeaders["content-type"]) && text.length > 0) try {
|
|
205
|
+
parsedBody = JSON.parse(text);
|
|
206
|
+
} catch {
|
|
207
|
+
parsedBody = text;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
status: response.status,
|
|
211
|
+
headers: responseHeaders,
|
|
212
|
+
body: parsedBody,
|
|
213
|
+
truncated
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
export { httpRequestTool };
|
|
221
|
+
//# sourceMappingURL=http-request.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-request.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-tools/src/http/http-request.ts"],"sourcesContent":["import { type ToolContract, tool } from \"@warlock.js/ai\";\nimport type {\n HttpMethod,\n HttpRequestInput,\n HttpRequestOptions,\n HttpRequestResult,\n} from \"../contracts\";\nimport { HttpPolicyError } from \"../errors\";\nimport {\n objectSchema,\n optionalStringEnumField,\n optionalStringRecordField,\n passthroughField,\n stringField,\n} from \"../schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"http_request\";\n\n/** Default per-request wall-clock timeout, in milliseconds. */\nconst DEFAULT_TIMEOUT_MS = 15_000;\n\n/** Default hard cap on response-body bytes before truncation. */\nconst DEFAULT_MAX_BYTES = 1_000_000;\n\n/** The full set of HTTP methods, in the order they appear in {@link HttpMethod}. */\nconst ALL_METHODS: readonly HttpMethod[] = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"];\n\n/** Methods that conventionally carry no request body — `body` is dropped for these. */\nconst BODYLESS_METHODS: ReadonlySet<HttpMethod> = new Set<HttpMethod>([\"GET\"]);\n\n/**\n * Standard Schema for {@link HttpRequestInput}. `url` is required;\n * `method` is constrained to the canonical HTTP verb set (further\n * narrowed to the tool's `allowMethods` at runtime); `headers` is an\n * optional string-to-string record; `body` is an opaque passthrough the\n * handler serializes based on its runtime type.\n */\nconst httpRequestInputSchema = objectSchema<HttpRequestInput>({\n method: optionalStringEnumField<HttpMethod>(ALL_METHODS),\n url: stringField(),\n headers: optionalStringRecordField(),\n body: passthroughField(),\n});\n\n/**\n * Resolve the request target. With a `baseUrl` configured the model\n * supplies a path joined against it; otherwise the model's `url` must be\n * an absolute `http(s)` URL. Throws a typed {@link HttpPolicyError} of\n * type `\"invalid-url\"` when the result cannot be parsed or is not an\n * `http`/`https` URL — surfaced as `{ error }` data, never a crash.\n */\nfunction resolveUrl(rawUrl: string, baseUrl: string | undefined): URL {\n let resolved: URL;\n\n try {\n // `new URL(input, base)` joins relative paths against `base` and\n // ignores `base` when `input` is already absolute, which is exactly\n // the \"path vs full URL\" behavior the design specifies.\n resolved = baseUrl !== undefined ? new URL(rawUrl, baseUrl) : new URL(rawUrl);\n } catch {\n throw new HttpPolicyError(\n `http_request could not resolve a valid URL from \"${rawUrl}\"` +\n (baseUrl !== undefined ? ` against base \"${baseUrl}\".` : \".\"),\n { type: \"invalid-url\" },\n );\n }\n\n if (resolved.protocol !== \"http:\" && resolved.protocol !== \"https:\") {\n throw new HttpPolicyError(\n `http_request only permits http(s) URLs; got \"${resolved.protocol}\".`,\n { type: \"invalid-url\" },\n );\n }\n\n return resolved;\n}\n\n/**\n * Read a `Response` body, capping at `maxBytes`. Returns the decoded text\n * and whether it was cut off. Streams chunk-by-chunk so an oversized body\n * is abandoned at the cap rather than fully buffered; falls back to\n * `response.text()` (then a post-hoc byte slice) when the body is not a\n * readable stream (e.g. a stubbed `Response` in tests).\n */\nasync function readCappedBody(\n response: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const body = response.body;\n\n if (!body) {\n return { text: \"\", truncated: false };\n }\n\n const decoder = new TextDecoder();\n const reader = body.getReader();\n let received = 0;\n let truncated = false;\n let text = \"\";\n\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n if (!value) {\n continue;\n }\n\n const remaining = maxBytes - received;\n\n if (value.byteLength > remaining) {\n text += decoder.decode(value.subarray(0, remaining), { stream: true });\n received = maxBytes;\n truncated = true;\n break;\n }\n\n text += decoder.decode(value, { stream: true });\n received += value.byteLength;\n }\n } finally {\n // Release the lock and abandon any unread remainder.\n await reader.cancel().catch(() => undefined);\n reader.releaseLock();\n }\n\n text += decoder.decode();\n\n return { text, truncated };\n}\n\n/**\n * Decide whether a response's `content-type` indicates JSON. Matches\n * `application/json` and the `+json` structured-suffix convention\n * (e.g. `application/vnd.api+json`), case-insensitively.\n */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n if (!contentType) {\n return false;\n }\n\n const value = contentType.toLowerCase();\n\n return value.includes(\"application/json\") || value.includes(\"+json\");\n}\n\n/**\n * Build the agent-facing `http_request` tool — a guarded HTTP/REST client\n * over the global `fetch`. The `options` bound what the model may do; the\n * model supplies the per-call URL / method / headers / body within those\n * rails.\n *\n * **Guardrails (all enforced before the network call).**\n * - **Method allowlist** — `allowMethods` (default `[\"GET\"]`). A method\n * outside the list is rejected with a typed\n * {@link HttpPolicyError} (`type: \"method-not-allowed\"`).\n * - **Host allowlist** — when `allowHosts` is set, any other host is\n * rejected (`type: \"host-not-allowed\"`), an SSRF guardrail.\n * - **`baseUrl` join** — when configured, the model passes a path that\n * is resolved against `baseUrl`; otherwise it must pass an absolute\n * `http(s)` URL. An unresolvable URL is rejected\n * (`type: \"invalid-url\"`).\n *\n * **Request shaping.** Static `options.headers` are merged under the\n * per-call `headers` (the per-call value wins). An object `body` is\n * JSON-serialized with a `content-type: application/json` default; a\n * string `body` is sent verbatim; `body` is dropped for bodyless methods\n * (`GET`). The call is bounded by `timeoutMs` (default `15_000`) via an\n * `AbortController`, also wired to `ctx.signal` for cooperative\n * cancellation.\n *\n * **Response shaping.** Headers are returned with lower-cased keys. The\n * body is read up to `maxBytes` (default `1_000_000`) and JSON-parsed\n * when the response `content-type` is JSON, otherwise returned as text;\n * `truncated` is `true` when the body was cut off at the cap (a truncated\n * JSON body is returned as the raw partial string, since it can no longer\n * be parsed).\n *\n * **Errors flow as data.** Every guardrail rejection and network failure\n * is thrown inside `execute`; the framework's `tool()` wrapper catches it\n * and surfaces it in the returned `{ error }` field, so the agent reads\n * the failure and self-corrects rather than crashing.\n *\n * @param options - Construction-time policy bounding the tool.\n * @returns A {@link ToolContract} the agent can call as `http_request`.\n *\n * @example\n * const stripe = httpRequestTool({\n * baseUrl: \"https://api.stripe.com\",\n * allowHosts: [\"api.stripe.com\"],\n * allowMethods: [\"GET\", \"POST\"],\n * headers: { authorization: `Bearer ${process.env.STRIPE_KEY}` },\n * });\n * const { data } = await stripe.invoke({ method: \"GET\", url: \"/v1/charges\" });\n */\nexport function httpRequestTool(\n options: HttpRequestOptions = {},\n): ToolContract<HttpRequestInput, HttpRequestResult> {\n const allowMethods = options.allowMethods ?? [\"GET\"];\n const allowedMethodSet = new Set<HttpMethod>(allowMethods);\n const allowHostSet = options.allowHosts ? new Set(options.allowHosts) : undefined;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n const staticHeaders = options.headers;\n\n return tool<HttpRequestInput, HttpRequestResult>({\n name: options.name ?? DEFAULT_NAME,\n description:\n \"Issue an HTTP request and return the status, response headers, and \" +\n \"parsed body. Allowed methods and hosts are restricted by the tool's \" +\n \"configuration; a request outside those rails is rejected before any \" +\n \"network call. Pass an object body to send JSON, or a string to send \" +\n \"it verbatim. The response body is JSON-parsed when the content-type \" +\n \"is JSON, otherwise returned as text, and is capped — `truncated` is \" +\n \"true when the body was cut off.\",\n action: (input) => `Requesting ${input.method ?? \"GET\"} ${input.url}`,\n input: httpRequestInputSchema,\n async execute(input, ctx) {\n const method: HttpMethod = input.method ?? \"GET\";\n\n // 1. Method allowlist — rejected before anything else.\n if (!allowedMethodSet.has(method)) {\n throw new HttpPolicyError(\n `http_request method \"${method}\" is not allowed. ` +\n `Permitted methods: ${[...allowedMethodSet].join(\", \")}.`,\n { type: \"method-not-allowed\" },\n );\n }\n\n // 2. URL resolution (baseUrl join when configured).\n const url = resolveUrl(input.url, options.baseUrl);\n\n // 3. Host allowlist — SSRF guardrail, before the fetch.\n if (allowHostSet && !allowHostSet.has(url.hostname)) {\n throw new HttpPolicyError(\n `http_request host \"${url.hostname}\" is not in the allowlist. ` +\n `Permitted hosts: ${[...allowHostSet].join(\", \")}.`,\n { type: \"host-not-allowed\" },\n );\n }\n\n // 4. Merge headers — static option headers under the per-call ones,\n // so a per-call header overrides a static default of the same name.\n const headers: Record<string, string> = { ...staticHeaders, ...input.headers };\n\n // 5. Shape the body. Dropped for bodyless methods; objects become\n // JSON (with a default content-type); strings are sent verbatim.\n let body: string | undefined;\n\n if (!BODYLESS_METHODS.has(method) && input.body !== undefined) {\n if (typeof input.body === \"string\") {\n body = input.body;\n } else {\n body = JSON.stringify(input.body);\n\n const hasContentType = Object.keys(headers).some(\n (key) => key.toLowerCase() === \"content-type\",\n );\n\n if (!hasContentType) {\n headers[\"content-type\"] = \"application/json\";\n }\n }\n }\n\n // 6. Bound the call by timeout, chained to the caller's signal.\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n const onAbort = () => controller.abort();\n\n if (ctx?.signal) {\n if (ctx.signal.aborted) {\n controller.abort();\n } else {\n ctx.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n }\n\n let response: Response;\n\n try {\n response = await fetch(url, { method, headers, body, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n ctx?.signal?.removeEventListener(\"abort\", onAbort);\n }\n\n // 7. Collect response headers with lower-cased keys.\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key.toLowerCase()] = value;\n });\n\n // 8. Read the body up to the cap, then parse-or-pass.\n const { text, truncated } = await readCappedBody(response, maxBytes);\n\n let parsedBody: unknown = text;\n\n // A truncated body can no longer be valid JSON, so only attempt a\n // parse on a complete JSON response; otherwise hand back the raw text.\n if (!truncated && isJsonContentType(responseHeaders[\"content-type\"]) && text.length > 0) {\n try {\n parsedBody = JSON.parse(text);\n } catch {\n // Content-type claimed JSON but the body was not — fall back to\n // the raw text rather than failing the whole call.\n parsedBody = text;\n }\n }\n\n return {\n status: response.status,\n headers: responseHeaders,\n body: parsedBody,\n truncated,\n };\n },\n });\n}\n"],"mappings":"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"}
|
package/esm/index.d.mts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { FetchUrlExtract, FetchUrlInput, FetchUrlOptions, FetchUrlResult, SearchProvider, WebSearchInput, WebSearchOptions, WebSearchResult, WebSearchResultItem } from "./contracts/web.type.mjs";
|
|
2
|
+
import { HttpMethod, HttpRequestInput, HttpRequestOptions, HttpRequestResult } from "./contracts/http.type.mjs";
|
|
3
|
+
import { CalculatorInput, CalculatorOptions, CalculatorResult, DateTimeInput, DateTimeOp, DateTimeOptions, DateTimeResult } from "./contracts/utility.type.mjs";
|
|
4
|
+
import { JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpServeOptions, McpServeSource, McpServeTransport, McpServer, McpToolCallResult, McpToolDescriptor, McpTransport } from "./contracts/mcp.type.mjs";
|
|
5
|
+
import { createServeHandler, serve } from "./mcp/serve.mjs";
|
|
6
|
+
import { jsonSchemaToStandard } from "./mcp/json-schema-to-standard.mjs";
|
|
7
|
+
import { McpTransportClient } from "./mcp/transport.type.mjs";
|
|
8
|
+
import { JsonRpcClientHandle, createJsonRpcClient, createTransport } from "./mcp/transport.mjs";
|
|
9
|
+
import { McpFactory, mcp } from "./mcp/index.mjs";
|
|
10
|
+
import { AiToolsNamespace } from "./register.mjs";
|
|
11
|
+
import { webSearchTool } from "./web/web-search.mjs";
|
|
12
|
+
import { fetchUrlTool } from "./web/fetch-url.mjs";
|
|
13
|
+
import { httpRequestTool } from "./http/http-request.mjs";
|
|
14
|
+
import { calculatorTool } from "./utility/calculator.mjs";
|
|
15
|
+
import { dateTimeTool } from "./utility/date-time.mjs";
|
|
16
|
+
import { CalculatorError, CalculatorErrorOptions, CalculatorFailure, DateTimeError, DateTimeErrorOptions, DateTimeFailure, HttpPolicyError, HttpPolicyErrorOptions, HttpPolicyFailure, McpTransportError, McpTransportErrorOptions, McpTransportFailure, WebToolError, WebToolErrorOptions, WebToolFailure } from "./errors.mjs";
|
|
17
|
+
export { type AiToolsNamespace, CalculatorError, type CalculatorErrorOptions, type CalculatorFailure, type CalculatorInput, type CalculatorOptions, type CalculatorResult, DateTimeError, type DateTimeErrorOptions, type DateTimeFailure, type DateTimeInput, type DateTimeOp, type DateTimeOptions, type DateTimeResult, type FetchUrlExtract, type FetchUrlInput, type FetchUrlOptions, type FetchUrlResult, type HttpMethod, HttpPolicyError, type HttpPolicyErrorOptions, type HttpPolicyFailure, type HttpRequestInput, type HttpRequestOptions, type HttpRequestResult, type JsonRpcClientHandle, type JsonRpcError, type JsonRpcId, type JsonRpcMessage, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type JsonRpcVersion, type McpClient, type McpClientOptions, type McpContentBlock, type McpFactory, type McpServeOptions, type McpServeSource, type McpServeTransport, type McpServer, type McpToolCallResult, type McpToolDescriptor, type McpTransport, type McpTransportClient, McpTransportError, type McpTransportErrorOptions, type McpTransportFailure, type SearchProvider, type WebSearchInput, type WebSearchOptions, type WebSearchResult, type WebSearchResultItem, WebToolError, type WebToolErrorOptions, type WebToolFailure, calculatorTool, createJsonRpcClient, createServeHandler, createTransport, dateTimeTool, fetchUrlTool, httpRequestTool, jsonSchemaToStandard, mcp, serve, webSearchTool };
|
|
18
|
+
import "./mcp/index.mjs";
|
|
19
|
+
import "./register.mjs";
|
package/esm/index.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CalculatorError, DateTimeError, HttpPolicyError, McpTransportError, WebToolError } from "./errors.mjs";
|
|
2
|
+
import { httpRequestTool } from "./http/http-request.mjs";
|
|
3
|
+
import { jsonSchemaToStandard } from "./mcp/json-schema-to-standard.mjs";
|
|
4
|
+
import { createJsonRpcClient, createTransport } from "./mcp/transport.mjs";
|
|
5
|
+
import { createServeHandler, serve } from "./mcp/serve.mjs";
|
|
6
|
+
import { mcp } from "./mcp/index.mjs";
|
|
7
|
+
import { calculatorTool } from "./utility/calculator.mjs";
|
|
8
|
+
import { dateTimeTool } from "./utility/date-time.mjs";
|
|
9
|
+
import { fetchUrlTool } from "./web/fetch-url.mjs";
|
|
10
|
+
import { webSearchTool } from "./web/web-search.mjs";
|
|
11
|
+
import "./register.mjs";
|
|
12
|
+
import "./web/index.mjs";
|
|
13
|
+
import "./utility/index.mjs";
|
|
14
|
+
|
|
15
|
+
export { CalculatorError, DateTimeError, HttpPolicyError, McpTransportError, WebToolError, calculatorTool, createJsonRpcClient, createServeHandler, createTransport, dateTimeTool, fetchUrlTool, httpRequestTool, jsonSchemaToStandard, mcp, serve, webSearchTool };
|