@writ-agent/sdk 0.1.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.
@@ -0,0 +1,109 @@
1
+ import { shouldDispatch } from "./client.js";
2
+ import { WritBlockedError, WritError } from "./errors.js";
3
+ import { fromRedacted, outputText } from "./output.js";
4
+ function defaultArgs(params) {
5
+ const first = params[0];
6
+ if (params.length === 1 && typeof first === "object" && first !== null && !Array.isArray(first)) {
7
+ return { ...first };
8
+ }
9
+ return { args: params };
10
+ }
11
+ /**
12
+ * Wrap a tool function so every invocation is decided by writ first and its
13
+ * execution recorded afterwards.
14
+ *
15
+ * - deny, rejected or unresolved ask: throws `WritBlockedError`; `fn` never runs.
16
+ * - any gateway failure before dispatch: throws `WritError`; `fn` never runs.
17
+ * - redact: returns writ's redacted output instead of the raw result.
18
+ * - `complete` failure after `fn` ran: throws `WritError` (the result is withheld).
19
+ */
20
+ export function guard(fn, options) {
21
+ const { client, tool } = options;
22
+ return async (...params) => {
23
+ const call = {
24
+ session_id: options.sessionId ?? client.sessionId,
25
+ tool,
26
+ args: options.args ? options.args(...params) : defaultArgs(params),
27
+ };
28
+ const callId = options.callId?.(...params);
29
+ if (callId !== undefined)
30
+ call.call_id = callId;
31
+ if (options.caller !== undefined)
32
+ call.caller = options.caller;
33
+ if (options.server !== undefined)
34
+ call.server = options.server;
35
+ if (options.trust !== undefined)
36
+ call.trust = options.trust;
37
+ const authorizeOptions = {
38
+ ...(options.approver !== undefined ? { approver: options.approver } : {}),
39
+ ...(options.approvalTimeoutMs !== undefined ? { approvalTimeoutMs: options.approvalTimeoutMs } : {}),
40
+ };
41
+ const decision = await client.authorize(call, authorizeOptions);
42
+ if (!shouldDispatch(decision))
43
+ throw new WritBlockedError(decision, tool);
44
+ const ref = decision.ref;
45
+ if (ref === undefined)
46
+ throw new WritError("protocol", "dispatching decision is missing ref");
47
+ let result;
48
+ try {
49
+ result = await fn(...params);
50
+ }
51
+ catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ await client.complete(ref, { ok: false, output: message }).catch(() => undefined);
54
+ throw err;
55
+ }
56
+ const text = outputText(result);
57
+ const recorded = await client.complete(ref, text === undefined ? { ok: true } : { ok: true, output: text });
58
+ if (decision.decision === "redact") {
59
+ if (text === undefined)
60
+ return result;
61
+ if (recorded.output === undefined) {
62
+ throw new WritError("redaction_missing", `writ: redact verdict for '${tool}' but no redacted output was returned; result withheld`);
63
+ }
64
+ return fromRedacted(recorded.output, result);
65
+ }
66
+ return result;
67
+ };
68
+ }
69
+ /**
70
+ * Wrap the `execute` of every tool in a record of tool objects (for example
71
+ * Vercel AI SDK `tools`). Tools without `execute` are returned unchanged.
72
+ * Extra `execute` parameters (e.g. the AI SDK's `{ toolCallId }`) are passed
73
+ * through, and `toolCallId` becomes writ's `call_id` when present.
74
+ */
75
+ export function guardTools(tools, options) {
76
+ const out = {};
77
+ for (const [key, tool] of Object.entries(tools)) {
78
+ const execute = tool.execute;
79
+ if (typeof execute !== "function") {
80
+ out[key] = tool;
81
+ continue;
82
+ }
83
+ const wrapped = guard((...params) => execute.apply(tool, params), {
84
+ client: options.client,
85
+ tool: options.toolName ? options.toolName(key) : key,
86
+ args: (...params) => {
87
+ const input = params[0];
88
+ if (options.args)
89
+ return options.args(key, input);
90
+ return typeof input === "object" && input !== null && !Array.isArray(input)
91
+ ? { ...input }
92
+ : { input };
93
+ },
94
+ callId: (...params) => {
95
+ const ctx = params[1];
96
+ if (typeof ctx === "object" && ctx !== null && typeof ctx.toolCallId === "string") {
97
+ return ctx.toolCallId;
98
+ }
99
+ return undefined;
100
+ },
101
+ ...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
102
+ ...(options.caller !== undefined ? { caller: options.caller } : {}),
103
+ ...(options.approver !== undefined ? { approver: options.approver } : {}),
104
+ ...(options.approvalTimeoutMs !== undefined ? { approvalTimeoutMs: options.approvalTimeoutMs } : {}),
105
+ });
106
+ out[key] = { ...tool, execute: wrapped };
107
+ }
108
+ return out;
109
+ }
@@ -0,0 +1,9 @@
1
+ export { WritClient, shouldDispatch } from "./client.js";
2
+ export type { ApprovalAnswer, ApprovalRequest, Approver, AskMode, AuthorizeOptions, WritClientOptions, } from "./client.js";
3
+ export { guard, guardTools } from "./guard.js";
4
+ export type { ExecutableTool, GuardOptions, GuardToolsOptions } from "./guard.js";
5
+ export { WritBlockedError, WritError, WritProtocolError, WritTimeoutError, WritUnavailableError, describeBlock, } from "./errors.js";
6
+ export { findOnPath, locateWrit } from "./locate.js";
7
+ export type { Launch } from "./locate.js";
8
+ export { PROTOCOL_VERSION } from "./protocol.js";
9
+ export type { CallerIdentity, CompleteInput, CompleteResult, Decision, DecisionKind, ServerIdentity, ToolCallInput, TrustVerdict, } from "./protocol.js";
@@ -0,0 +1,5 @@
1
+ export { WritClient, shouldDispatch } from "./client.js";
2
+ export { guard, guardTools } from "./guard.js";
3
+ export { WritBlockedError, WritError, WritProtocolError, WritTimeoutError, WritUnavailableError, describeBlock, } from "./errors.js";
4
+ export { findOnPath, locateWrit } from "./locate.js";
5
+ export { PROTOCOL_VERSION } from "./protocol.js";
@@ -0,0 +1,18 @@
1
+ /** A resolved program + leading arguments to spawn. */
2
+ export interface Launch {
3
+ command: string;
4
+ args: string[];
5
+ }
6
+ /**
7
+ * Turn an explicit binary path into a launch. A `.js`/`.mjs`/`.cjs` path runs
8
+ * under the current Node executable (useful for test gateways on Windows,
9
+ * where scripts are not directly executable).
10
+ */
11
+ export declare function launchFor(bin: string): Launch;
12
+ /** Search PATH for `writ` (`writ.exe` / `writ.com` on Windows). */
13
+ export declare function findOnPath(name?: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
14
+ /**
15
+ * Locate writ: explicit `bin`, then `WRIT_BIN`, then PATH. Throws
16
+ * `WritUnavailableError` when nothing is found.
17
+ */
18
+ export declare function locateWrit(bin?: string, env?: NodeJS.ProcessEnv): Launch;
@@ -0,0 +1,59 @@
1
+ import { statSync } from "node:fs";
2
+ import { delimiter, extname, isAbsolute, join, resolve } from "node:path";
3
+ import { WritUnavailableError } from "./errors.js";
4
+ const SCRIPT_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]);
5
+ function isFile(p) {
6
+ try {
7
+ return statSync(p).isFile();
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ /**
14
+ * Turn an explicit binary path into a launch. A `.js`/`.mjs`/`.cjs` path runs
15
+ * under the current Node executable (useful for test gateways on Windows,
16
+ * where scripts are not directly executable).
17
+ */
18
+ export function launchFor(bin) {
19
+ const full = isAbsolute(bin) ? bin : resolve(bin);
20
+ if (!isFile(full)) {
21
+ throw new WritUnavailableError(`writ binary not found at '${full}' (fail closed: no tool call will run)`);
22
+ }
23
+ if (SCRIPT_EXTENSIONS.has(extname(full).toLowerCase())) {
24
+ return { command: process.execPath, args: [full] };
25
+ }
26
+ return { command: full, args: [] };
27
+ }
28
+ /** Search PATH for `writ` (`writ.exe` / `writ.com` on Windows). */
29
+ export function findOnPath(name = "writ", env = process.env, platform = process.platform) {
30
+ const pathVar = env.PATH ?? env.Path ?? env.path ?? "";
31
+ const dirs = pathVar.split(platform === "win32" ? ";" : delimiter).filter((d) => d.length > 0);
32
+ // Batch files (.cmd/.bat) need a shell to run; they are not accepted.
33
+ const names = platform === "win32" ? [`${name}.exe`, `${name}.com`] : [name];
34
+ for (const dir of dirs) {
35
+ const clean = dir.replace(/^"(.*)"$/, "$1");
36
+ for (const n of names) {
37
+ const candidate = join(clean, n);
38
+ if (isFile(candidate))
39
+ return candidate;
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+ /**
45
+ * Locate writ: explicit `bin`, then `WRIT_BIN`, then PATH. Throws
46
+ * `WritUnavailableError` when nothing is found.
47
+ */
48
+ export function locateWrit(bin, env = process.env) {
49
+ if (bin !== undefined && bin !== "")
50
+ return launchFor(bin);
51
+ const fromEnv = env.WRIT_BIN;
52
+ if (fromEnv !== undefined && fromEnv !== "")
53
+ return launchFor(fromEnv);
54
+ const found = findOnPath("writ", env);
55
+ if (found === undefined) {
56
+ throw new WritUnavailableError("writ binary not found: set WRIT_BIN or put writ on PATH (fail closed: no tool call will run)");
57
+ }
58
+ return { command: found, args: [] };
59
+ }
@@ -0,0 +1,10 @@
1
+ /** Render a tool result as the text writ hashes (and redacts). */
2
+ export declare function outputText(result: unknown): string | undefined;
3
+ /**
4
+ * Map writ's redacted text back onto the original result's shape: strings stay
5
+ * strings; structured results are re-parsed from the redacted JSON when that
6
+ * still parses, otherwise the redacted text itself is returned.
7
+ */
8
+ export declare function fromRedacted(redacted: string, original: unknown): unknown;
9
+ /** Replacement text when a result must be withheld (redaction could not be applied). */
10
+ export declare const WITHHELD_OUTPUT = "[writ: tool output withheld because redaction could not be applied]";
@@ -0,0 +1,33 @@
1
+ /** Render a tool result as the text writ hashes (and redacts). */
2
+ export function outputText(result) {
3
+ if (result === undefined)
4
+ return undefined;
5
+ if (typeof result === "string")
6
+ return result;
7
+ if (result instanceof Uint8Array)
8
+ return Buffer.from(result).toString("utf8");
9
+ try {
10
+ const json = JSON.stringify(result);
11
+ return json === undefined ? String(result) : json;
12
+ }
13
+ catch {
14
+ return String(result);
15
+ }
16
+ }
17
+ /**
18
+ * Map writ's redacted text back onto the original result's shape: strings stay
19
+ * strings; structured results are re-parsed from the redacted JSON when that
20
+ * still parses, otherwise the redacted text itself is returned.
21
+ */
22
+ export function fromRedacted(redacted, original) {
23
+ if (typeof original === "string" || original === undefined || original instanceof Uint8Array)
24
+ return redacted;
25
+ try {
26
+ return JSON.parse(redacted);
27
+ }
28
+ catch {
29
+ return redacted;
30
+ }
31
+ }
32
+ /** Replacement text when a result must be withheld (redaction could not be applied). */
33
+ export const WITHHELD_OUTPUT = "[writ: tool output withheld because redaction could not be applied]";
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Wire types for the writ hook gateway (`writ check --stdio`), protocol v1.
3
+ * Source of truth: docs/INTERFACES.md, Contract 6 (and Contract 1 for the
4
+ * `call` envelope). Additive-only: unknown response fields are ignored.
5
+ */
6
+ export declare const PROTOCOL_VERSION: 1;
7
+ /** Who is making the call (Contract 1 `CallerIdentity`). */
8
+ export interface CallerIdentity {
9
+ agent: string;
10
+ agent_version?: string | null;
11
+ user?: string | null;
12
+ non_human_id?: string | null;
13
+ }
14
+ /** Identity of the (MCP) server a call targets (Contract 1 `ServerIdentity`). */
15
+ export interface ServerIdentity {
16
+ name: string;
17
+ /** "stdio" | "sse" | "http", or another transport label. */
18
+ transport: string;
19
+ version?: string | null;
20
+ }
21
+ /** External scanner verdict (Contract 1 `TrustVerdict`). */
22
+ export type TrustVerdict = "verified" | "unverified" | "malicious";
23
+ /**
24
+ * The `call` object of a `decide` request. writ sets `mode = SdkHook` and
25
+ * `captured_at` itself. Credentials must never appear in `args`.
26
+ */
27
+ export interface ToolCallInput {
28
+ /** Optional; writ generates one when omitted. */
29
+ call_id?: string;
30
+ session_id: string;
31
+ tool: string;
32
+ args: Record<string, unknown>;
33
+ /** Defaults to the client's `caller`, then `{ agent: "unknown" }`. */
34
+ caller?: CallerIdentity;
35
+ server?: ServerIdentity | null;
36
+ trust?: TrustVerdict | null;
37
+ }
38
+ export type DecisionKind = "allow" | "deny" | "ask" | "redact";
39
+ /** A validated `decide` or `resolve` response. */
40
+ export interface Decision {
41
+ decision: DecisionKind;
42
+ /** True only for allow / redact / approved ask. */
43
+ dispatch: boolean;
44
+ /** Opaque handle to pass back to `resolve` / `complete`. */
45
+ ref?: string;
46
+ rule_id?: string;
47
+ reason?: string;
48
+ location?: string;
49
+ /** "required" when `--ask defer` deferred the ask to the adapter. */
50
+ approval?: "required";
51
+ irreversible?: boolean;
52
+ timeout_ms?: number;
53
+ /** Redaction patterns (writ applies them on `complete`). */
54
+ patterns?: string[];
55
+ }
56
+ /** A validated `complete` response. */
57
+ export interface CompleteResult {
58
+ recorded: true;
59
+ /** Redacted output; present only for a redact verdict. */
60
+ output?: string;
61
+ }
62
+ export interface CompleteInput {
63
+ ok: boolean;
64
+ exit?: number;
65
+ /** Tool result as text. Hashed into the ledger, never stored. */
66
+ output?: string;
67
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Wire types for the writ hook gateway (`writ check --stdio`), protocol v1.
3
+ * Source of truth: docs/INTERFACES.md, Contract 6 (and Contract 1 for the
4
+ * `call` envelope). Additive-only: unknown response fields are ignored.
5
+ */
6
+ export const PROTOCOL_VERSION = 1;
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@writ-agent/sdk",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript/Node client for the writ hook gateway: policy-checked, ledger-recorded tool calls for AI agents (Claude Agent SDK hooks, generic tool wrappers).",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/writ-agent/writ.git",
10
+ "directory": "adapters/typescript"
11
+ },
12
+ "keywords": [
13
+ "writ",
14
+ "ai-agents",
15
+ "authorization",
16
+ "policy",
17
+ "audit-ledger",
18
+ "claude-agent-sdk",
19
+ "tool-calls"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "sideEffects": false,
25
+ "main": "./dist/cjs/index.js",
26
+ "module": "./dist/esm/index.js",
27
+ "types": "./dist/cjs/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "import": {
31
+ "types": "./dist/esm/index.d.ts",
32
+ "default": "./dist/esm/index.js"
33
+ },
34
+ "require": {
35
+ "types": "./dist/cjs/index.d.ts",
36
+ "default": "./dist/cjs/index.js"
37
+ }
38
+ },
39
+ "./claude-agent-sdk": {
40
+ "import": {
41
+ "types": "./dist/esm/claude-agent-sdk.d.ts",
42
+ "default": "./dist/esm/claude-agent-sdk.js"
43
+ },
44
+ "require": {
45
+ "types": "./dist/cjs/claude-agent-sdk.d.ts",
46
+ "default": "./dist/cjs/claude-agent-sdk.js"
47
+ }
48
+ },
49
+ "./package.json": "./package.json"
50
+ },
51
+ "typesVersions": {
52
+ "*": {
53
+ "claude-agent-sdk": [
54
+ "./dist/cjs/claude-agent-sdk.d.ts"
55
+ ]
56
+ }
57
+ },
58
+ "files": [
59
+ "dist",
60
+ "README.md",
61
+ "LICENSE"
62
+ ],
63
+ "scripts": {
64
+ "clean": "node scripts/clean.mjs dist build-test",
65
+ "build": "node scripts/clean.mjs dist && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json && node scripts/postbuild.mjs",
66
+ "typecheck": "tsc -p tsconfig.json",
67
+ "lint": "node scripts/lint.mjs",
68
+ "build:test": "node scripts/clean.mjs build-test && tsc -p tsconfig.test.json",
69
+ "test": "npm run build:test && node --test --test-concurrency=1 \"build-test/test/*.test.js\"",
70
+ "test:e2e": "npm run build:test && node --test \"build-test/test/e2e.test.js\"",
71
+ "prepublishOnly": "npm run build && npm run typecheck && npm run lint && npm test"
72
+ },
73
+ "peerDependencies": {
74
+ "@anthropic-ai/claude-agent-sdk": "^0.3.280"
75
+ },
76
+ "peerDependenciesMeta": {
77
+ "@anthropic-ai/claude-agent-sdk": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@anthropic-ai/claude-agent-sdk": "0.3.280",
83
+ "@types/node": "^24.13.6",
84
+ "typescript": "~5.9",
85
+ "zod": "^4.6.5"
86
+ },
87
+ "author": "Bhaskar Gurram <gurrambhaskar.ai@gmail.com>",
88
+ "publishConfig": {
89
+ "access": "public"
90
+ },
91
+ "homepage": "https://github.com/writ-agent/writ/tree/main/adapters/typescript#readme",
92
+ "bugs": {
93
+ "url": "https://github.com/writ-agent/writ/issues"
94
+ }
95
+ }