@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,387 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WritClient = void 0;
4
+ exports.shouldDispatch = shouldDispatch;
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_crypto_1 = require("node:crypto");
7
+ const errors_js_1 = require("./errors.js");
8
+ const locate_js_1 = require("./locate.js");
9
+ const protocol_js_1 = require("./protocol.js");
10
+ const DECISIONS = new Set(["allow", "deny", "ask", "redact"]);
11
+ const DEFAULT_APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
12
+ /** True only when a decision says the tool may run. */
13
+ function shouldDispatch(decision) {
14
+ return decision.dispatch === true && decision.decision !== "deny";
15
+ }
16
+ /**
17
+ * A long-lived `writ check --stdio` child process speaking protocol v1.
18
+ * Every failure (missing binary, crash, timeout, malformed line, `error`
19
+ * response) rejects with a `WritError`; callers must then not run the tool.
20
+ */
21
+ class WritClient {
22
+ askMode;
23
+ sessionId;
24
+ caller;
25
+ options;
26
+ timeoutMs;
27
+ maxLineBytes;
28
+ proc;
29
+ queue = [];
30
+ buffer = "";
31
+ seq = 0;
32
+ closed = false;
33
+ broken;
34
+ stderrTail = "";
35
+ constructor(options = {}) {
36
+ this.options = options;
37
+ this.askMode = options.ask ?? "deny";
38
+ if (this.askMode !== "deny" && this.askMode !== "defer") {
39
+ throw new errors_js_1.WritError("bad_option", `ask must be "deny" or "defer", got ${String(options.ask)}`);
40
+ }
41
+ this.timeoutMs = options.timeoutMs ?? 30_000;
42
+ this.maxLineBytes = options.maxLineBytes ?? 16 * 1024 * 1024;
43
+ this.sessionId = options.sessionId ?? (0, node_crypto_1.randomUUID)();
44
+ this.caller = options.caller;
45
+ }
46
+ /** The argv (after the program) passed to writ. */
47
+ gatewayArgs() {
48
+ const args = [];
49
+ if (this.options.policy !== undefined)
50
+ args.push("--policy", this.options.policy);
51
+ if (this.options.ledger !== undefined)
52
+ args.push("--ledger", this.options.ledger);
53
+ args.push("check", "--stdio", "--ask", this.askMode);
54
+ return args;
55
+ }
56
+ /** Ask writ for a verdict. Rejects on any gateway failure. */
57
+ async decide(call) {
58
+ const body = {
59
+ op: "decide",
60
+ call: {
61
+ ...call,
62
+ session_id: call.session_id || this.sessionId,
63
+ caller: call.caller ?? this.caller ?? { agent: "unknown" },
64
+ server: call.server ?? null,
65
+ trust: call.trust ?? null,
66
+ },
67
+ };
68
+ return parseDecision(await this.request(body), "decide");
69
+ }
70
+ /** Resolve a deferred ask. The response is a final decision. */
71
+ async resolve(ref, approved, approver) {
72
+ const body = { op: "resolve", ref, approved };
73
+ if (approver !== undefined)
74
+ body.approver = approver;
75
+ const decision = parseDecision(await this.request(body), "resolve");
76
+ if (!approved && shouldDispatch(decision)) {
77
+ throw new errors_js_1.WritProtocolError("gateway returned dispatch:true for a rejected ask");
78
+ }
79
+ return decision;
80
+ }
81
+ /** Record the execution of a dispatched call. For redact, `output` in the result is the redacted text. */
82
+ async complete(ref, input) {
83
+ const body = { op: "complete", ref, ok: input.ok };
84
+ if (input.exit !== undefined)
85
+ body.exit = input.exit;
86
+ if (input.output !== undefined)
87
+ body.output = input.output;
88
+ const res = await this.request(body);
89
+ if (res.recorded !== true) {
90
+ throw new errors_js_1.WritProtocolError("complete response is missing recorded:true");
91
+ }
92
+ if (res.output !== undefined && typeof res.output !== "string") {
93
+ throw new errors_js_1.WritProtocolError("complete response output is not a string");
94
+ }
95
+ return typeof res.output === "string" ? { recorded: true, output: res.output } : { recorded: true };
96
+ }
97
+ /**
98
+ * decide, and for a deferred ask obtain an answer from `approver` and
99
+ * `resolve` it. Returns the final decision; check `shouldDispatch`.
100
+ * A missing, throwing, late or non-approving approver is a denial.
101
+ */
102
+ async authorize(call, options = {}) {
103
+ const decision = await this.decide(call);
104
+ if (decision.decision !== "ask" || decision.approval !== "required")
105
+ return decision;
106
+ if (decision.ref === undefined)
107
+ throw new errors_js_1.WritProtocolError("deferred ask is missing ref");
108
+ const { approved, approver } = await runApprover(call, decision, options);
109
+ return this.resolve(decision.ref, approved, approver);
110
+ }
111
+ /** Close stdin, wait briefly for writ to exit, then kill it. Idempotent. */
112
+ async close() {
113
+ this.closed = true;
114
+ const proc = this.proc;
115
+ if (proc === undefined)
116
+ return;
117
+ await new Promise((done) => {
118
+ if (proc.exitCode !== null || proc.signalCode !== null)
119
+ return done();
120
+ const timer = setTimeout(() => {
121
+ proc.kill();
122
+ done();
123
+ }, 2000);
124
+ proc.once("exit", () => {
125
+ clearTimeout(timer);
126
+ done();
127
+ });
128
+ proc.stdin?.end();
129
+ });
130
+ this.failAll(new errors_js_1.WritError("closed", "writ client closed"));
131
+ this.proc = undefined;
132
+ }
133
+ async [Symbol.asyncDispose]() {
134
+ await this.close();
135
+ }
136
+ // --- transport -----------------------------------------------------------
137
+ request(body) {
138
+ if (this.closed)
139
+ return Promise.reject(new errors_js_1.WritError("closed", "writ client is closed"));
140
+ if (this.broken !== undefined && this.options.respawn === false)
141
+ return Promise.reject(this.broken);
142
+ let proc;
143
+ try {
144
+ proc = this.ensureProcess();
145
+ }
146
+ catch (err) {
147
+ return Promise.reject(toWritError(err));
148
+ }
149
+ const id = `r${++this.seq}`;
150
+ const op = String(body.op);
151
+ const line = JSON.stringify({ v: protocol_js_1.PROTOCOL_VERSION, id, ...body }) + "\n";
152
+ return new Promise((resolve, reject) => {
153
+ const timer = setTimeout(() => {
154
+ this.poison(new errors_js_1.WritTimeoutError(`writ check did not answer ${op} ${id} within ${this.timeoutMs} ms`));
155
+ }, this.timeoutMs);
156
+ this.queue.push({ id, op, resolve, reject, timer });
157
+ this.setRef(true);
158
+ const stdin = proc.stdin;
159
+ if (stdin === null || !stdin.writable) {
160
+ this.poison(new errors_js_1.WritProtocolError("writ check stdin is not writable"));
161
+ return;
162
+ }
163
+ stdin.write(line);
164
+ });
165
+ }
166
+ ensureProcess() {
167
+ if (this.proc !== undefined)
168
+ return this.proc;
169
+ const launch = this.resolveLaunch();
170
+ const args = [...launch.args, ...this.gatewayArgs()];
171
+ const proc = (0, node_child_process_1.spawn)(launch.command, args, {
172
+ cwd: this.options.cwd,
173
+ env: this.options.env ?? process.env,
174
+ stdio: ["pipe", "pipe", "pipe"],
175
+ shell: false,
176
+ windowsHide: true,
177
+ });
178
+ this.proc = proc;
179
+ this.broken = undefined;
180
+ this.buffer = "";
181
+ this.stderrTail = "";
182
+ proc.stdout?.setEncoding("utf8");
183
+ proc.stderr?.setEncoding("utf8");
184
+ proc.stdout?.on("data", (chunk) => {
185
+ if (this.proc === proc)
186
+ this.onData(chunk);
187
+ });
188
+ proc.stderr?.on("data", (chunk) => {
189
+ this.stderrTail = (this.stderrTail + chunk).slice(-4096);
190
+ this.options.onStderr?.(chunk);
191
+ });
192
+ proc.stdin?.on("error", (err) => {
193
+ if (this.proc === proc)
194
+ this.poison(new errors_js_1.WritProtocolError(`writ check stdin failed: ${err.message}`, { cause: err }));
195
+ });
196
+ proc.on("error", (err) => {
197
+ if (this.proc !== proc)
198
+ return;
199
+ const code = err.code;
200
+ this.poison(code === "ENOENT" || code === "EACCES"
201
+ ? new errors_js_1.WritUnavailableError(`cannot start writ (${launch.command}): ${err.message}`, { cause: err })
202
+ : new errors_js_1.WritProtocolError(`writ check failed: ${err.message}`, { cause: err }));
203
+ });
204
+ // "close" fires after stdout/stderr are drained, so the stderr tail is complete.
205
+ proc.on("close", (code, signal) => {
206
+ if (this.proc !== proc)
207
+ return;
208
+ const how = signal !== null ? `signal ${signal}` : `code ${String(code)}`;
209
+ const tail = this.stderrTail.trim();
210
+ this.poison(new errors_js_1.WritProtocolError(`writ check exited (${how})${tail ? `: ${tail}` : ""}`));
211
+ });
212
+ return proc;
213
+ }
214
+ resolveLaunch() {
215
+ if (this.options.command !== undefined) {
216
+ return { command: this.options.command, args: [...(this.options.args ?? [])] };
217
+ }
218
+ const launch = this.options.bin !== undefined ? (0, locate_js_1.launchFor)(this.options.bin) : (0, locate_js_1.locateWrit)(undefined, this.options.env ?? process.env);
219
+ return { command: launch.command, args: [...launch.args, ...(this.options.args ?? [])] };
220
+ }
221
+ onData(chunk) {
222
+ this.buffer += chunk;
223
+ let nl;
224
+ while ((nl = this.buffer.indexOf("\n")) >= 0) {
225
+ const raw = this.buffer.slice(0, nl).replace(/\r$/, "");
226
+ this.buffer = this.buffer.slice(nl + 1);
227
+ if (raw.trim() === "")
228
+ continue;
229
+ this.onLine(raw);
230
+ if (this.proc === undefined)
231
+ return;
232
+ }
233
+ if (Buffer.byteLength(this.buffer, "utf8") > this.maxLineBytes) {
234
+ this.poison(new errors_js_1.WritProtocolError(`writ check response line exceeds ${this.maxLineBytes} bytes`));
235
+ }
236
+ }
237
+ onLine(raw) {
238
+ let msg;
239
+ try {
240
+ msg = JSON.parse(raw);
241
+ }
242
+ catch {
243
+ this.poison(new errors_js_1.WritProtocolError(`malformed line from writ check: ${truncate(raw)}`));
244
+ return;
245
+ }
246
+ const head = this.queue[0];
247
+ if (head === undefined) {
248
+ this.poison(new errors_js_1.WritProtocolError(`unsolicited line from writ check: ${truncate(raw)}`));
249
+ return;
250
+ }
251
+ if (!isObject(msg) || msg.v !== protocol_js_1.PROTOCOL_VERSION || msg.id !== head.id) {
252
+ this.poison(new errors_js_1.WritProtocolError(`unexpected response (wanted v:1 id:${head.id}): ${truncate(raw)}`));
253
+ return;
254
+ }
255
+ this.queue.shift();
256
+ clearTimeout(head.timer);
257
+ if (this.queue.length === 0)
258
+ this.setRef(false);
259
+ if (msg.error !== undefined) {
260
+ const e = isObject(msg.error) ? msg.error : {};
261
+ const code = typeof e.code === "string" ? e.code : "error";
262
+ const message = typeof e.message === "string" ? e.message : "writ check returned an error";
263
+ head.reject(new errors_js_1.WritError(code, `writ ${head.op} failed (${code}): ${message}`));
264
+ return;
265
+ }
266
+ head.resolve(msg);
267
+ }
268
+ /** Fail every in-flight request and drop the process (fail closed). */
269
+ poison(error) {
270
+ const proc = this.proc;
271
+ this.proc = undefined;
272
+ this.broken = error;
273
+ this.buffer = "";
274
+ if (proc !== undefined && proc.exitCode === null && proc.signalCode === null) {
275
+ proc.kill();
276
+ }
277
+ this.failAll(error);
278
+ }
279
+ failAll(error) {
280
+ const pending = this.queue;
281
+ this.queue = [];
282
+ for (const p of pending) {
283
+ clearTimeout(p.timer);
284
+ p.reject(error);
285
+ }
286
+ }
287
+ /** Keep the event loop alive only while requests are in flight. */
288
+ setRef(on) {
289
+ const proc = this.proc;
290
+ if (proc === undefined)
291
+ return;
292
+ const method = on ? "ref" : "unref";
293
+ proc[method]();
294
+ for (const s of [proc.stdin, proc.stdout, proc.stderr]) {
295
+ const handle = s;
296
+ handle?.[method]?.();
297
+ }
298
+ }
299
+ }
300
+ exports.WritClient = WritClient;
301
+ // --- helpers ---------------------------------------------------------------
302
+ async function runApprover(call, decision, options) {
303
+ const approver = options.approver;
304
+ if (approver === undefined)
305
+ return { approved: false, approver: "adapter:no-approver" };
306
+ const limit = decision.timeout_ms ?? options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
307
+ const controller = new AbortController();
308
+ let timer;
309
+ const timeout = new Promise((done) => {
310
+ timer = setTimeout(() => {
311
+ controller.abort();
312
+ done("timeout");
313
+ }, limit);
314
+ });
315
+ try {
316
+ const answer = await Promise.race([
317
+ Promise.resolve().then(() => approver({ call, decision, signal: controller.signal })),
318
+ timeout,
319
+ ]);
320
+ if (answer === "timeout")
321
+ return { approved: false, approver: "adapter:approval-timeout" };
322
+ if (answer === true)
323
+ return { approved: true, approver: "adapter:approver" };
324
+ if (isObject(answer) && answer.approved === true) {
325
+ return { approved: true, approver: typeof answer.approver === "string" ? answer.approver : "adapter:approver" };
326
+ }
327
+ const who = isObject(answer) && typeof answer.approver === "string" ? answer.approver : "adapter:approver";
328
+ return { approved: false, approver: who };
329
+ }
330
+ catch {
331
+ return { approved: false, approver: "adapter:approver-error" };
332
+ }
333
+ finally {
334
+ if (timer !== undefined)
335
+ clearTimeout(timer);
336
+ }
337
+ }
338
+ function parseDecision(msg, op) {
339
+ const kind = msg.decision;
340
+ if (typeof kind !== "string" || !DECISIONS.has(kind)) {
341
+ throw new errors_js_1.WritProtocolError(`${op} response has no valid decision`);
342
+ }
343
+ if (typeof msg.dispatch !== "boolean") {
344
+ throw new errors_js_1.WritProtocolError(`${op} response has no boolean dispatch`);
345
+ }
346
+ const decision = { decision: kind, dispatch: msg.dispatch };
347
+ // A decision that contradicts itself is not trusted.
348
+ if (kind === "deny" && msg.dispatch)
349
+ throw new errors_js_1.WritProtocolError(`${op}: deny with dispatch:true`);
350
+ if (op === "decide" && kind === "ask" && msg.dispatch)
351
+ throw new errors_js_1.WritProtocolError("decide: ask with dispatch:true");
352
+ if (op === "decide" && (kind === "allow" || kind === "redact") && !msg.dispatch) {
353
+ throw new errors_js_1.WritProtocolError(`decide: ${kind} with dispatch:false`);
354
+ }
355
+ if (typeof msg.ref === "string")
356
+ decision.ref = msg.ref;
357
+ if (typeof msg.rule_id === "string")
358
+ decision.rule_id = msg.rule_id;
359
+ if (typeof msg.reason === "string")
360
+ decision.reason = msg.reason;
361
+ if (typeof msg.location === "string")
362
+ decision.location = msg.location;
363
+ if (msg.approval === "required")
364
+ decision.approval = "required";
365
+ if (typeof msg.irreversible === "boolean")
366
+ decision.irreversible = msg.irreversible;
367
+ if (typeof msg.timeout_ms === "number")
368
+ decision.timeout_ms = msg.timeout_ms;
369
+ if (Array.isArray(msg.patterns))
370
+ decision.patterns = msg.patterns.filter((p) => typeof p === "string");
371
+ if (decision.dispatch && decision.ref === undefined) {
372
+ throw new errors_js_1.WritProtocolError(`${op}: dispatching decision is missing ref`);
373
+ }
374
+ return decision;
375
+ }
376
+ function isObject(v) {
377
+ return typeof v === "object" && v !== null && !Array.isArray(v);
378
+ }
379
+ function truncate(s) {
380
+ return s.length > 200 ? `${s.slice(0, 200)}...` : s;
381
+ }
382
+ function toWritError(err) {
383
+ if (err instanceof errors_js_1.WritError)
384
+ return err;
385
+ const message = err instanceof Error ? err.message : String(err);
386
+ return new errors_js_1.WritUnavailableError(`cannot start writ: ${message}`, { cause: err });
387
+ }
@@ -0,0 +1,35 @@
1
+ import type { Decision } from "./protocol.js";
2
+ /**
3
+ * Base class for every writ failure. Any `WritError` means the tool call must
4
+ * not run (fail closed).
5
+ */
6
+ export declare class WritError extends Error {
7
+ /** Machine-readable code, e.g. `unavailable`, `timeout`, `protocol`, or a gateway `error.code`. */
8
+ readonly code: string;
9
+ constructor(code: string, message: string, options?: {
10
+ cause?: unknown;
11
+ });
12
+ }
13
+ /** The writ binary could not be found or started. */
14
+ export declare class WritUnavailableError extends WritError {
15
+ constructor(message: string, options?: {
16
+ cause?: unknown;
17
+ });
18
+ }
19
+ /** The gateway did not answer within the per-request timeout. */
20
+ export declare class WritTimeoutError extends WritError {
21
+ constructor(message: string);
22
+ }
23
+ /** The gateway wrote something that is not a valid protocol v1 response, or exited. */
24
+ export declare class WritProtocolError extends WritError {
25
+ constructor(message: string, options?: {
26
+ cause?: unknown;
27
+ });
28
+ }
29
+ /** writ decided the call must not run (deny, rejected or unresolved ask). */
30
+ export declare class WritBlockedError extends WritError {
31
+ readonly decision: Decision;
32
+ constructor(decision: Decision, tool: string);
33
+ }
34
+ /** Human-readable reason for a non-dispatching decision, naming the rule. */
35
+ export declare function describeBlock(decision: Decision, tool: string): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WritBlockedError = exports.WritProtocolError = exports.WritTimeoutError = exports.WritUnavailableError = exports.WritError = void 0;
4
+ exports.describeBlock = describeBlock;
5
+ /**
6
+ * Base class for every writ failure. Any `WritError` means the tool call must
7
+ * not run (fail closed).
8
+ */
9
+ class WritError extends Error {
10
+ /** Machine-readable code, e.g. `unavailable`, `timeout`, `protocol`, or a gateway `error.code`. */
11
+ code;
12
+ constructor(code, message, options) {
13
+ super(message, options);
14
+ this.name = "WritError";
15
+ this.code = code;
16
+ }
17
+ }
18
+ exports.WritError = WritError;
19
+ /** The writ binary could not be found or started. */
20
+ class WritUnavailableError extends WritError {
21
+ constructor(message, options) {
22
+ super("unavailable", message, options);
23
+ this.name = "WritUnavailableError";
24
+ }
25
+ }
26
+ exports.WritUnavailableError = WritUnavailableError;
27
+ /** The gateway did not answer within the per-request timeout. */
28
+ class WritTimeoutError extends WritError {
29
+ constructor(message) {
30
+ super("timeout", message);
31
+ this.name = "WritTimeoutError";
32
+ }
33
+ }
34
+ exports.WritTimeoutError = WritTimeoutError;
35
+ /** The gateway wrote something that is not a valid protocol v1 response, or exited. */
36
+ class WritProtocolError extends WritError {
37
+ constructor(message, options) {
38
+ super("protocol", message, options);
39
+ this.name = "WritProtocolError";
40
+ }
41
+ }
42
+ exports.WritProtocolError = WritProtocolError;
43
+ /** writ decided the call must not run (deny, rejected or unresolved ask). */
44
+ class WritBlockedError extends WritError {
45
+ decision;
46
+ constructor(decision, tool) {
47
+ super("blocked", describeBlock(decision, tool));
48
+ this.name = "WritBlockedError";
49
+ this.decision = decision;
50
+ }
51
+ }
52
+ exports.WritBlockedError = WritBlockedError;
53
+ /** Human-readable reason for a non-dispatching decision, naming the rule. */
54
+ function describeBlock(decision, tool) {
55
+ const rule = decision.rule_id ? `rule '${decision.rule_id}'` : "policy default";
56
+ const where = decision.location ? ` (${decision.location})` : "";
57
+ const why = decision.reason ? `: ${decision.reason}` : "";
58
+ const verb = decision.decision === "ask" ? "needs approval and was not approved" : "was blocked";
59
+ return `writ: '${tool}' ${verb} by ${rule}${where}${why}`;
60
+ }
@@ -0,0 +1,56 @@
1
+ import { type Approver, type WritClient } from "./client.js";
2
+ import type { CallerIdentity, ServerIdentity, TrustVerdict } from "./protocol.js";
3
+ export interface GuardOptions<A extends unknown[]> {
4
+ client: WritClient;
5
+ /** Tool name as writ policies see it, e.g. `bash`, `fs.read`, `postgres.query`. */
6
+ tool: string;
7
+ /**
8
+ * Build the policy-visible `args` from the call's parameters. Default: the
9
+ * first parameter when it is a plain object, else `{ args: [...params] }`.
10
+ * Never include credentials.
11
+ */
12
+ args?: (...params: A) => Record<string, unknown>;
13
+ /** Stable id for this call (default: writ generates one). */
14
+ callId?: (...params: A) => string | undefined;
15
+ /** Default: the client's session id. */
16
+ sessionId?: string;
17
+ caller?: CallerIdentity;
18
+ server?: ServerIdentity | null;
19
+ trust?: TrustVerdict | null;
20
+ /** Decides deferred asks (client with `ask: "defer"`). Default: reject. */
21
+ approver?: Approver;
22
+ approvalTimeoutMs?: number;
23
+ }
24
+ /**
25
+ * Wrap a tool function so every invocation is decided by writ first and its
26
+ * execution recorded afterwards.
27
+ *
28
+ * - deny, rejected or unresolved ask: throws `WritBlockedError`; `fn` never runs.
29
+ * - any gateway failure before dispatch: throws `WritError`; `fn` never runs.
30
+ * - redact: returns writ's redacted output instead of the raw result.
31
+ * - `complete` failure after `fn` ran: throws `WritError` (the result is withheld).
32
+ */
33
+ export declare function guard<A extends unknown[], R>(fn: (...params: A) => R | Promise<R>, options: GuardOptions<A>): (...params: A) => Promise<Awaited<R>>;
34
+ /** A `{ description, parameters, execute }` tool object (Vercel AI SDK and similar). */
35
+ export interface ExecutableTool {
36
+ execute?: (...params: never[]) => unknown;
37
+ [key: string]: unknown;
38
+ }
39
+ export interface GuardToolsOptions {
40
+ client: WritClient;
41
+ /** Map a tool key to the writ tool name (default: the key itself). */
42
+ toolName?: (key: string) => string;
43
+ /** Build policy-visible args from the tool's first parameter (default: the parameter itself). */
44
+ args?: (key: string, input: unknown) => Record<string, unknown>;
45
+ sessionId?: string;
46
+ caller?: CallerIdentity;
47
+ approver?: Approver;
48
+ approvalTimeoutMs?: number;
49
+ }
50
+ /**
51
+ * Wrap the `execute` of every tool in a record of tool objects (for example
52
+ * Vercel AI SDK `tools`). Tools without `execute` are returned unchanged.
53
+ * Extra `execute` parameters (e.g. the AI SDK's `{ toolCallId }`) are passed
54
+ * through, and `toolCallId` becomes writ's `call_id` when present.
55
+ */
56
+ export declare function guardTools<T extends Record<string, ExecutableTool>>(tools: T, options: GuardToolsOptions): T;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.guard = guard;
4
+ exports.guardTools = guardTools;
5
+ const client_js_1 = require("./client.js");
6
+ const errors_js_1 = require("./errors.js");
7
+ const output_js_1 = require("./output.js");
8
+ function defaultArgs(params) {
9
+ const first = params[0];
10
+ if (params.length === 1 && typeof first === "object" && first !== null && !Array.isArray(first)) {
11
+ return { ...first };
12
+ }
13
+ return { args: params };
14
+ }
15
+ /**
16
+ * Wrap a tool function so every invocation is decided by writ first and its
17
+ * execution recorded afterwards.
18
+ *
19
+ * - deny, rejected or unresolved ask: throws `WritBlockedError`; `fn` never runs.
20
+ * - any gateway failure before dispatch: throws `WritError`; `fn` never runs.
21
+ * - redact: returns writ's redacted output instead of the raw result.
22
+ * - `complete` failure after `fn` ran: throws `WritError` (the result is withheld).
23
+ */
24
+ function guard(fn, options) {
25
+ const { client, tool } = options;
26
+ return async (...params) => {
27
+ const call = {
28
+ session_id: options.sessionId ?? client.sessionId,
29
+ tool,
30
+ args: options.args ? options.args(...params) : defaultArgs(params),
31
+ };
32
+ const callId = options.callId?.(...params);
33
+ if (callId !== undefined)
34
+ call.call_id = callId;
35
+ if (options.caller !== undefined)
36
+ call.caller = options.caller;
37
+ if (options.server !== undefined)
38
+ call.server = options.server;
39
+ if (options.trust !== undefined)
40
+ call.trust = options.trust;
41
+ const authorizeOptions = {
42
+ ...(options.approver !== undefined ? { approver: options.approver } : {}),
43
+ ...(options.approvalTimeoutMs !== undefined ? { approvalTimeoutMs: options.approvalTimeoutMs } : {}),
44
+ };
45
+ const decision = await client.authorize(call, authorizeOptions);
46
+ if (!(0, client_js_1.shouldDispatch)(decision))
47
+ throw new errors_js_1.WritBlockedError(decision, tool);
48
+ const ref = decision.ref;
49
+ if (ref === undefined)
50
+ throw new errors_js_1.WritError("protocol", "dispatching decision is missing ref");
51
+ let result;
52
+ try {
53
+ result = await fn(...params);
54
+ }
55
+ catch (err) {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ await client.complete(ref, { ok: false, output: message }).catch(() => undefined);
58
+ throw err;
59
+ }
60
+ const text = (0, output_js_1.outputText)(result);
61
+ const recorded = await client.complete(ref, text === undefined ? { ok: true } : { ok: true, output: text });
62
+ if (decision.decision === "redact") {
63
+ if (text === undefined)
64
+ return result;
65
+ if (recorded.output === undefined) {
66
+ throw new errors_js_1.WritError("redaction_missing", `writ: redact verdict for '${tool}' but no redacted output was returned; result withheld`);
67
+ }
68
+ return (0, output_js_1.fromRedacted)(recorded.output, result);
69
+ }
70
+ return result;
71
+ };
72
+ }
73
+ /**
74
+ * Wrap the `execute` of every tool in a record of tool objects (for example
75
+ * Vercel AI SDK `tools`). Tools without `execute` are returned unchanged.
76
+ * Extra `execute` parameters (e.g. the AI SDK's `{ toolCallId }`) are passed
77
+ * through, and `toolCallId` becomes writ's `call_id` when present.
78
+ */
79
+ function guardTools(tools, options) {
80
+ const out = {};
81
+ for (const [key, tool] of Object.entries(tools)) {
82
+ const execute = tool.execute;
83
+ if (typeof execute !== "function") {
84
+ out[key] = tool;
85
+ continue;
86
+ }
87
+ const wrapped = guard((...params) => execute.apply(tool, params), {
88
+ client: options.client,
89
+ tool: options.toolName ? options.toolName(key) : key,
90
+ args: (...params) => {
91
+ const input = params[0];
92
+ if (options.args)
93
+ return options.args(key, input);
94
+ return typeof input === "object" && input !== null && !Array.isArray(input)
95
+ ? { ...input }
96
+ : { input };
97
+ },
98
+ callId: (...params) => {
99
+ const ctx = params[1];
100
+ if (typeof ctx === "object" && ctx !== null && typeof ctx.toolCallId === "string") {
101
+ return ctx.toolCallId;
102
+ }
103
+ return undefined;
104
+ },
105
+ ...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
106
+ ...(options.caller !== undefined ? { caller: options.caller } : {}),
107
+ ...(options.approver !== undefined ? { approver: options.approver } : {}),
108
+ ...(options.approvalTimeoutMs !== undefined ? { approvalTimeoutMs: options.approvalTimeoutMs } : {}),
109
+ });
110
+ out[key] = { ...tool, execute: wrapped };
111
+ }
112
+ return out;
113
+ }
@@ -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";