@crawlcheck/sdk 1.5.2 → 1.6.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 CHANGED
@@ -20,6 +20,25 @@ Every verification returns `integrity_valid`, `issuer_trusted` and `accepted`. A
20
20
 
21
21
  `preflight(domain, action, { agent, template, policy })` returns the signed receipt without deciding for you; `verifyDocument(doc)` verifies any signed resolve answer or decision receipt offline. `resolve(domain)` returns the full signed ResolveV1 answer.
22
22
 
23
+ ## Give your agent the check as a tool
24
+
25
+ Two tools your agent can call itself: `crawlcheck_check_site` (the signed, verified decision for an action on a site) and `crawlcheck_fetch` (fetches a page only after its domain, and every redirect hop, is allowed). Both fail closed.
26
+
27
+ ```js
28
+ import { CrawlCheck, agentTools, toolsFor } from "@crawlcheck/sdk";
29
+ const tools = agentTools(new CrawlCheck(), { agent: "MyAgentBot" });
30
+
31
+ // OpenAI function calling
32
+ const oa = toolsFor("openai", tools); // oa.tools → tools: [...]; on a tool call: await oa.call(name, argumentsJson)
33
+ // Anthropic tool use
34
+ const an = toolsFor("anthropic", tools); // an.tools → tools: [...]; on tool_use: await an.call(block.name, block.input)
35
+ // Vercel AI SDK
36
+ import { tool, jsonSchema } from "ai";
37
+ const vt = Object.fromEntries(tools.map((t) => [t.name, tool({ description: t.description, parameters: jsonSchema(t.parameters), execute: t.execute })]));
38
+ // An MCP server you run
39
+ const mc = toolsFor("mcp", tools); // list mc.tools; answer tools/call with mc.call(name, args)
40
+ ```
41
+
23
42
  ## Read a record
24
43
 
25
44
  ```js
package/dist/index.d.ts CHANGED
@@ -686,4 +686,26 @@ export declare class CrawlCheck {
686
686
  }>;
687
687
  private req;
688
688
  }
689
+ /**
690
+ * Agent tools (1.6.0): CrawlCheck as two tools any agent framework can register, so the check happens inside the agent's
691
+ * own tool loop. crawlcheck_check_site returns the signed, verified decision for an action on a site; crawlcheck_fetch
692
+ * fetches a page only after its domain (and every redirect hop) proceeds. Both fail closed, exactly like guard().
693
+ * toolsFor() shapes them for OpenAI function calling, Anthropic tool use, the Vercel AI SDK or an MCP server.
694
+ */
695
+ export interface AgentTool {
696
+ name: string;
697
+ description: string;
698
+ parameters: JsonObject;
699
+ execute: (args: any) => Promise<JsonObject>;
700
+ }
701
+ export interface AgentToolOptions extends GuardOptions {
702
+ maxChars?: number;
703
+ }
704
+ export declare function agentTools(cc: CrawlCheck, opts?: AgentToolOptions): AgentTool[];
705
+ export type ToolFormat = "openai" | "anthropic" | "vercel" | "mcp";
706
+ /** Shape agentTools() for a framework. call(name, args) runs a tool by name for formats that hand you the call (OpenAI, Anthropic, MCP). For the Vercel AI SDK wrap each parameters object: tool({ description, parameters: jsonSchema(t.parameters), execute: t.execute }). */
707
+ export declare function toolsFor(format: ToolFormat, tools: AgentTool[]): {
708
+ tools: unknown;
709
+ call: (name: string, args: unknown) => Promise<JsonObject>;
710
+ };
689
711
  export default CrawlCheck;
package/dist/index.js CHANGED
@@ -563,4 +563,63 @@ export class CrawlCheck {
563
563
  return j;
564
564
  }
565
565
  }
566
+ const AT_ACTIONS = ["read", "cite", "connect", "transact", "administer"];
567
+ function atDomain(v) { const s = String(v ?? "").trim(); try {
568
+ return normalizeDomain(/^[a-z][a-z0-9+.-]*:\/\//i.test(s) ? new URL(s).hostname : s);
569
+ }
570
+ catch {
571
+ return "";
572
+ } }
573
+ function atAction(v) { return AT_ACTIONS.includes(String(v)) ? String(v) : "read"; }
574
+ export function agentTools(cc, opts = {}) {
575
+ const max = opts.maxChars ?? 20000;
576
+ return [
577
+ { name: "crawlcheck_check_site",
578
+ description: "Before acting on a website, get CrawlCheck's signed decision for that action: allow, warn, require_confirmation, block or unsupported, with the reasons. Use it before reading, citing, connecting to, buying from or administering a site you have not checked. proceed is false whenever the site should not be used for that action.",
579
+ parameters: { type: "object", additionalProperties: false, required: ["site"], properties: {
580
+ site: { type: "string", description: "The site's domain or any URL on it, e.g. example.com or https://example.com/page" },
581
+ action: { type: "string", enum: AT_ACTIONS, description: "What you are about to do: read (default), cite, connect (an API or MCP server), transact (pay, book, submit personal data) or administer" }
582
+ } },
583
+ execute: async (a) => {
584
+ const domain = atDomain(a?.site), action = atAction(a?.action);
585
+ if (!domain)
586
+ return { proceed: false, why: ["not a public domain name: " + String(a?.site ?? "")] };
587
+ const g = await cc.guard(domain, action, opts);
588
+ return { domain, action, proceed: g.proceed, decision: g.receipt ? String(g.receipt.decision) : null, why: g.why, reasons: Array.isArray(g.receipt?.steps) ? g.receipt.steps.map((x) => String(x?.why ?? "")).filter(Boolean) : [], reason_codes: g.receipt?.reason_codes ?? [],
589
+ receipt_sha256: g.receipt?.sha256 ?? null, verified: g.verification ? g.verification.accepted : false, alternatives: g.receipt?.alternatives ?? null };
590
+ } },
591
+ { name: "crawlcheck_fetch",
592
+ description: "Fetch a web page only if CrawlCheck allows reading that site; every redirect to a new domain is checked again before it is followed. Returns the page text (truncated) or, when refused, why. Prefer this over a plain fetch for sites you have not checked.",
593
+ parameters: { type: "object", additionalProperties: false, required: ["url"], properties: {
594
+ url: { type: "string", description: "The http(s) URL to fetch" },
595
+ action: { type: "string", enum: ["read", "cite"], description: "read (default) or cite, if the text will be quoted back to someone" }
596
+ } },
597
+ execute: async (a) => {
598
+ const r = await cc.guardedFetch(String(a?.url ?? ""), Object.assign({}, opts, { action: (a?.action === "cite" ? "cite" : "read") }));
599
+ const hops = r.hops.map((h) => ({ url: h.url, domain: h.domain, decision: h.decision, proceed: h.proceed, status: h.status ?? null }));
600
+ if (!r.proceed || !r.response)
601
+ return { proceed: false, url: r.url, why: r.why, hops };
602
+ const text = await r.response.text();
603
+ return { proceed: true, url: r.url, status: r.response.status, content_type: r.response.headers.get("content-type"), text: text.slice(0, max), truncated: text.length > max, chars: text.length, hops };
604
+ } }
605
+ ];
606
+ }
607
+ /** Shape agentTools() for a framework. call(name, args) runs a tool by name for formats that hand you the call (OpenAI, Anthropic, MCP). For the Vercel AI SDK wrap each parameters object: tool({ description, parameters: jsonSchema(t.parameters), execute: t.execute }). */
608
+ export function toolsFor(format, tools) {
609
+ const by = new Map(tools.map((t) => [t.name, t]));
610
+ const call = async (name, args) => { const t = by.get(name); if (!t)
611
+ return { proceed: false, why: ["no CrawlCheck tool named " + name] }; return t.execute(typeof args === "string" ? (() => { try {
612
+ return JSON.parse(args);
613
+ }
614
+ catch {
615
+ return {};
616
+ } })() : (args ?? {})); };
617
+ if (format === "openai")
618
+ return { tools: tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } })), call };
619
+ if (format === "anthropic")
620
+ return { tools: tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters })), call };
621
+ if (format === "mcp")
622
+ return { tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.parameters, annotations: { readOnlyHint: true, openWorldHint: true } })), call };
623
+ return { tools: Object.fromEntries(tools.map((t) => [t.name, { description: t.description, parameters: t.parameters, execute: t.execute }])), call };
624
+ }
566
625
  export default CrawlCheck;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlcheck/sdk",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Typed client for the CrawlCheck API (generated from its OpenAPI document) and the zero-dependency offline verifier for evidence bundles and remediation receipts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,37 @@
1
+ // Agent tools (1.6.0): shapes offline, decisions against the live API. npm test
2
+ import test from "node:test"; import assert from "node:assert/strict";
3
+ import { CrawlCheck, agentTools, toolsFor } from "../dist/index.js";
4
+ const cc = new CrawlCheck();
5
+ const tools = agentTools(cc);
6
+
7
+ test("agent tools: two tools, each with a JSON-schema object and an execute()", () => {
8
+ assert.deepEqual(tools.map((t) => t.name), ["crawlcheck_check_site", "crawlcheck_fetch"]);
9
+ for (const t of tools) { assert.equal(t.parameters.type, "object"); assert.equal(typeof t.execute, "function"); assert.ok(t.description.length > 40); }
10
+ });
11
+
12
+ test("toolsFor: OpenAI, Anthropic, MCP and Vercel shapes carry the same schema", () => {
13
+ const o = toolsFor("openai", tools).tools, a = toolsFor("anthropic", tools).tools, m = toolsFor("mcp", tools).tools, v = toolsFor("vercel", tools).tools;
14
+ assert.equal(o[0].type, "function"); assert.equal(o[0].function.parameters, tools[0].parameters);
15
+ assert.equal(a[1].input_schema, tools[1].parameters); assert.equal(m[0].inputSchema, tools[0].parameters);
16
+ assert.equal(typeof v.crawlcheck_fetch.execute, "function");
17
+ });
18
+
19
+ test("check_site fails closed on a non-domain without calling the API", async () => {
20
+ const r = await toolsFor("openai", tools).call("crawlcheck_check_site", JSON.stringify({ site: "not a domain" }));
21
+ assert.equal(r.proceed, false);
22
+ assert.equal((await toolsFor("openai", tools).call("no_such_tool", {})).proceed, false);
23
+ });
24
+
25
+ test("live: check_site returns a verified, signed decision for crawlcheck.io", async () => {
26
+ const r = await tools[0].execute({ site: "https://crawlcheck.io/pricing", action: "read" });
27
+ assert.equal(r.domain, "crawlcheck.io");
28
+ assert.ok(["allow", "warn", "require_confirmation", "block", "unsupported"].includes(r.decision), JSON.stringify(r));
29
+ assert.match(String(r.receipt_sha256), /^[0-9a-f]{64}$/);
30
+ assert.equal(r.verified, true);
31
+ });
32
+
33
+ test("live: fetch returns page text only after the check proceeds", async () => {
34
+ const r = await tools[1].execute({ url: "https://crawlcheck.io/llms.txt" });
35
+ if (r.proceed) { assert.equal(r.status, 200); assert.ok(r.text.length > 100); assert.ok(r.hops.length >= 1 && r.hops[0].proceed); }
36
+ else assert.ok(r.why.length, "a refusal names why");
37
+ });