@cygnuxlabs/tracekit 0.0.0-stage → 1.2.1

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.
Files changed (40) hide show
  1. package/README.md +294 -2
  2. package/bin/tracekit.mjs +30 -0
  3. package/dist/v1.d.ts +97 -0
  4. package/dist/v1.js +535 -0
  5. package/dist/v2/adapters/claude-agent-sdk.d.ts +45 -0
  6. package/dist/v2/adapters/claude-agent-sdk.js +142 -0
  7. package/dist/v2/adapters/gate.d.ts +62 -0
  8. package/dist/v2/adapters/gate.js +195 -0
  9. package/dist/v2/adapters/openai-agents.d.ts +85 -0
  10. package/dist/v2/adapters/openai-agents.js +114 -0
  11. package/dist/v2/adapters/vercel-ai.d.ts +51 -0
  12. package/dist/v2/adapters/vercel-ai.js +132 -0
  13. package/dist/v2/client.d.ts +159 -0
  14. package/dist/v2/client.js +749 -0
  15. package/dist/v2/executor.d.ts +49 -0
  16. package/dist/v2/executor.js +57 -0
  17. package/dist/v2/jcs.d.ts +5 -0
  18. package/dist/v2/jcs.js +12 -0
  19. package/dist/v2/launcher.d.ts +10 -0
  20. package/dist/v2/launcher.js +50 -0
  21. package/dist/v2/rpc_schema.d.ts +3 -0
  22. package/dist/v2/rpc_schema.js +43 -0
  23. package/dist/v2/validate.d.ts +6 -0
  24. package/dist/v2/validate.js +70 -0
  25. package/dist/verify/cli.d.ts +1 -0
  26. package/dist/verify/cli.js +46 -0
  27. package/dist/verify/detect.d.ts +41 -0
  28. package/dist/verify/detect.js +289 -0
  29. package/dist/verify/index.d.ts +34 -0
  30. package/dist/verify/index.js +930 -0
  31. package/dist/verify/jcs.d.ts +19 -0
  32. package/dist/verify/jcs.js +154 -0
  33. package/dist/verify/primitives.d.ts +62 -0
  34. package/dist/verify/primitives.js +298 -0
  35. package/dist/verify/schema.d.ts +11 -0
  36. package/dist/verify/schema.js +98 -0
  37. package/dist/verify/tracekit.event.v2.json +4588 -0
  38. package/dist/verify/zip.d.ts +17 -0
  39. package/dist/verify/zip.js +145 -0
  40. package/package.json +42 -4
package/README.md CHANGED
@@ -1,3 +1,295 @@
1
- # Temporary Holding Version
1
+ # @cygnuxlabs/tracekit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Tracekit for TypeScript and JavaScript agents: every tool call is decided by the Tracekit signer before it runs and
4
+ recorded after, and model responses become signed evidence. The client talks to the signer directly; it needs no Python
5
+ at runtime (the same-user dev signer is started with Python when none is running).
6
+
7
+ ## Install
8
+
9
+ Node 18 or newer:
10
+
11
+ ```sh
12
+ npm install @cygnuxlabs/tracekit
13
+ ```
14
+
15
+ npm also installs the signer package for your platform (`@cygnuxlabs/tracekit-signer-<platform>`: Apple Silicon
16
+ macOS, Linux x64 and arm64 with glibc, Windows x64), which bundles a Python, so `npx @cygnuxlabs/tracekit up` starts a
17
+ dev signer without one. On an Intel Mac or Alpine there is none: `pip install 'tracekit-ai[signer]'`.
18
+
19
+ Or build it from a checkout and install the file it makes:
20
+
21
+ ```sh
22
+ cd sdk/typescript && npm ci && npm run build && npm pack # writes cygnuxlabs-tracekit-<version>.tgz
23
+ cd /path/to/your/app && npm install /path/to/Tracekit/sdk/typescript/cygnuxlabs-tracekit-*.tgz
24
+ ```
25
+
26
+ ## Signer client (`@cygnuxlabs/tracekit`)
27
+
28
+ A native client of the v2 signer RPC with no runtime dependencies (Node ≥ 18 built-ins only, no Python bridge); also
29
+ exported as `@cygnuxlabs/tracekit/v2`.
30
+
31
+ ```ts
32
+ import { Client, withRun, currentRun } from "@cygnuxlabs/tracekit";
33
+
34
+ const client = new Client(); // or new Client({ signer: "https://signer:8443" })
35
+ const run = await client.registerRun("research-bot");
36
+ const d = await run.decide(call.id, "Bash", call.function.arguments); // the raw model string, or a parsed value
37
+ if (d.decision === "allow") {
38
+ const out = await runShell(call);
39
+ await run.complete(call.id, "ok", { result: out }); // never throws: a recording failure is a warning
40
+ } else if (d.decision === "ask") {
41
+ const { approval_id } = await run.approvalRequest(call.id);
42
+ const { state } = await run.approvalWait(approval_id, 300_000); // on a connection of its own
43
+ }
44
+ await run.close();
45
+ await client.close(); // waits for what is in flight
46
+ ```
47
+
48
+ - **Signer:** `$TRACEKIT_SIGNER` names a Unix socket, `tcp://127.0.0.1:port` (with `$TRACEKIT_SIGNER_TOKEN`; both sides
49
+ prove they hold it, it never goes on the wire) or `https://host:port` (`$TRACEKIT_SIGNER_TOKEN_FILE`: a bearer token
50
+ re-read per call; `$TRACEKIT_SIGNER_CERT`/`_KEY`: mTLS; `$TRACEKIT_SIGNER_CA`: the signer's CA). In system mode the
51
+ signer named by the root-owned `/etc/tracekit/client.json` is used and a different `$TRACEKIT_SIGNER` is refused.
52
+ Otherwise the same-user dev signer is found, or started with `tracekit up --json`: `$TRACEKIT_PYTHON -m tracekit` if
53
+ set, else a pip-installed `tracekit` on PATH, else the Python bundled in this platform's
54
+ `@cygnuxlabs/tracekit-signer-<platform>` package. `npx @cygnuxlabs/tracekit up|down|status|view|verify|signer` runs the
55
+ same command.
56
+ - **Protocol:** `hello` first; a signer that does not speak this client's RPC version is refused with `Incompatible`.
57
+ Requests are validated against the RPC schemas before they are sent, pipelined on one connection and answered in
58
+ order; a call whose connection drops is resent with the same `request_id`. Each handle's events carry the client's
59
+ `stream` and a `client_seq` per run.
60
+ - **Fail modes:** only when the signer cannot be reached does `decide` return the run's fail mode for the tool class
61
+ (`fields.tool_class_hint`, from `register_run`'s `fail_modes`, default closed) with `unavailable: true`. A refusal
62
+ from the signer rejects with `RPCError`, never an allow.
63
+ - **Serverless:** `client.waitUntil(promise)` and `await client.flush()` before the host freezes the process.
64
+ - `withRun(run, fn)` makes `run` the `currentRun()` of everything `fn` awaits (AsyncLocalStorage).
65
+ - `argsDigest(tool, args)`, `canonicalize`, `strictParse`: JCS (RFC 8785) and the strict parser, byte-identical to
66
+ the Python client on the shared vectors in `tests/vectors/jcs.jsonl`.
67
+
68
+ ## Verify a bundle (`@cygnuxlabs/tracekit/verify`)
69
+
70
+ An independent verifier of v2 bundles (`.tkb`), written from [docs/format-v2.md](../../docs/format-v2.md) and tested to
71
+ agree with the Python verifier. No dependencies: Web Crypto and `DecompressionStream`, so Node ≥ 20.12 and current
72
+ browsers.
73
+
74
+ ```sh
75
+ npx @cygnuxlabs/tracekit verify run.tkb --trust trust.json [--json] [--strict]
76
+ ```
77
+
78
+ Exit 0 verified, 1 failed, 2 unusable or unverifiable, 3 a warning under `--strict`; the report and `--json` are those
79
+ of `tracekit verify`. In code, Node or a browser:
80
+
81
+ ```ts
82
+ import { verify, formatReport } from "@cygnuxlabs/tracekit/verify";
83
+
84
+ const bundle = new Uint8Array(await (await fetch("run.tkb")).arrayBuffer());
85
+ const trust = new Uint8Array(await (await fetch("trust.json")).arrayBuffer()); // your own pinned trust config
86
+ const { report, code } = await verify(bundle, trust);
87
+ console.log(report.integrity, report.assurance); // e.g. "VERIFIED", "witnessed; records ed25519; ..."
88
+ ```
89
+
90
+ It does not check SLH-DSA checkpoint lines, Rekor anchors or certified record keys: a bundle that relies on one is
91
+ `UNVERIFIABLE (not checked by this verifier: ...)`, never `VERIFIED`. Monitor reports, revocations and the v1 format
92
+ bridge need the Python verifier.
93
+
94
+ ## v2 framework adapters
95
+
96
+ One call registers a run (the dev signer is started if none answers), closes it when the process is about to exit,
97
+ and hands you the adapters bound to it; then `tracekit last` exports, pins and verifies the run:
98
+
99
+ ```ts
100
+ import { instrument } from "@cygnuxlabs/tracekit";
101
+ const tk = await instrument(); // the same on a second call
102
+ const model = wrapLanguageModel({ model: openai("gpt-5"), middleware: tk.ai.middleware }); // Vercel AI SDK
103
+ const r = await generateText({ model, tools: tk.ai.tools({ pay }), toolApproval: tk.ai.toolApproval, messages });
104
+ const agent = new Agent({ name: "payer", tools: [tk.agents.tool(tool, { name: "pay", description, parameters, execute })] }); // OpenAI Agents JS
105
+ ```
106
+
107
+ ES modules can't be patched from outside, so unlike Python's `tracekit.instrument()` the adapters are passed to the
108
+ framework by you.
109
+
110
+ Each adapter gates every tool call through the signer: it passes the call id, its attempt and the model's raw
111
+ arguments, the signer decides, an `ask` waits for a person (held in the hook where the framework allows, else paused
112
+ and resumed), and every call, allowed or approved, runs only after the signer's `approval_consume` agrees. A `deny`
113
+ reaches the model as the tool's error result and the run goes on. The run's fail mode (`register_run`'s `fail_modes`)
114
+ applies only when the signer cannot be reached at the decision; a refusal, or a failure while waiting for an approval,
115
+ never lets the call run. Recording a finished call never throws and never changes its result (a failure is a
116
+ `TracekitWarning`). The frameworks are optional peer dependencies: the adapters import none of them, and the client
117
+ keeps no runtime dependencies. No adapter wraps a provider client, so the SDKs' own `APIPromise` (`.withResponse()`)
118
+ and streams (`messages.stream()`, `streamText`) work as without Tracekit. Offline examples: `examples/v2_*.mjs`.
119
+
120
+ - **A server: a run per conversation.** Register a run per conversation and make it current around each request with
121
+ `withRun(run, () => runner.run(...))`: every adapter decides the calls made inside it in that run (its complete and
122
+ approvals follow the call), and uses the run it was built with otherwise. `instrument()`'s own run is replaced by a
123
+ new one (a warning, linked as `parent_run` while the old one is closing) when the signer closed it after it was idle.
124
+ - **Exit.** `instrument()` closes its run on `beforeExit`, and on SIGINT or SIGTERM when nothing else handles the
125
+ signal (then the signal's default action runs). A signal your app or a library handles is left to it (`@openai/agents`
126
+ exits on SIGINT/SIGTERM): close the run in your own handler (`await tk.run.close()`) before you exit. A run stays
127
+ open while approvals of it wait for a person or, approved, to be resumed. To approve and resume in another process,
128
+ keep the run's `runId` and `runToken` with the framework's saved state and re-attach:
129
+ `new RunHandle(new Client(), { run_id, run_token })`; close that client when you are done with it
130
+ (`run.client.close()`), or re-attach every run through one shared client. `process.exit()` skips all of this: close
131
+ first.
132
+ - **The LLM gateway.** Point the provider at it with the run in a header, e.g. OpenAI Agents
133
+ `setDefaultOpenAIClient(new OpenAI({ baseURL, apiKey: <gateway token>, defaultHeaders: { "X-Tracekit-Run":
134
+ `${run.runId}.${run.runToken}` } }))`, Vercel AI `createOpenAI({ baseURL, apiKey, headers })`. Register the run as
135
+ the identity the gateway relays (`TRACEKIT_SIGNER=https://…` with the same token), and grant that identity the RPCs
136
+ the adapters use, `approval_list` included ([limits](../../docs/limits.md#deployment)). The adapters' own model
137
+ events are then optional: the gateway records each exchange too.
138
+
139
+ **OpenAI Agents SDK** (`@cygnuxlabs/tracekit/v2/openai-agents`)
140
+
141
+ ```ts
142
+ import { Agent, Runner, tool } from "@openai/agents";
143
+ import { TracekitAgents } from "@cygnuxlabs/tracekit/v2/openai-agents";
144
+ const tk = new TracekitAgents(await new Client().registerRun("payer"));
145
+ const agent = new Agent({ name: "payer", tools: [tk.tool(tool, { name: "pay", description, parameters, execute })] });
146
+ let result = await runner.run(agent, "pay acct-42 $15", { context: {} }); // the context must be an object
147
+ if (result.interruptions.length) { // an `ask`: once decided in the signer
148
+ await tk.applyDecisions(result.state);
149
+ result = await runner.run(agent, result.state);
150
+ }
151
+ await tk.recordResponses(result.rawResponses, "gpt-5");
152
+ ```
153
+
154
+ `tk.tool(tool, options)` builds the tool with the SDK's `tool()`: the signer's policy replaces `needsApproval`, and a
155
+ tool input guardrail (after your own) gates the call on its raw arguments. The approval id travels in the RunState only
156
+ as a hint (`context.tracekit.approvals`); the signer checks it. Leave `toolExecution.preApprovalInputGuardrails` off:
157
+ with it on, the guardrail runs before the interruption and every `ask` is refused at once.
158
+
159
+ A local MCP server (`MCPServerStdio`, SSE, streamable HTTP) is gated through its tool guardrails:
160
+ `new MCPServerStdio({ name: "filesystem", ..., ...tk.mcp("filesystem") })` decides each call as
161
+ `mcp:filesystem/<tool>`, holds an `ask` in the guardrail until a person decides, and completes the call with the
162
+ server's output. Pass the server's own `name`: with `mcpConfig.includeServerInToolNames` the model's
163
+ `mcp_<server>__<tool>` is turned back into `mcp:<server>/<tool>` from it. A call your own input guardrails refuse is
164
+ recorded as refused. Without it, local MCP calls
165
+ are not gated. A sub-agent used as a tool (`agent.asTool()`) is not gated either, and its model responses are not in
166
+ the outer `rawResponses`: gate its own tools with `tk.tool`, and record its responses (`customOutputExtractor` gets
167
+ the nested result) or run the LLM gateway. Also not gated, listed as uncovered: hosted tools (web and file search,
168
+ code interpreter, hosted MCP, hosted shell), shell, apply_patch and computer tools, and handoffs (name them in the
169
+ policy's `handoffs:` so they reconcile). `recordResponses` records them (T3) in a signed model event per response.
170
+
171
+ **Vercel AI SDK v7** (`@cygnuxlabs/tracekit/v2/vercel-ai`)
172
+
173
+ ```ts
174
+ import { generateText, wrapLanguageModel } from "ai";
175
+ import { tracekitAI } from "@cygnuxlabs/tracekit/v2/vercel-ai";
176
+ const tk = tracekitAI(await new Client().registerRun("assistant"));
177
+ const model = wrapLanguageModel({ model: openai("gpt-5"), middleware: tk.middleware });
178
+ const r = await generateText({ model, tools: tk.tools({ pay }), toolApproval: tk.toolApproval, messages });
179
+ // asks end the step with tool-approval-request parts; once decided in the signer:
180
+ messages.push(...r.response.messages, { role: "tool", content: await tk.approvalResponses(r.content) });
181
+ ```
182
+
183
+ The same options work for `streamText` and `ToolLoopAgent`. `tk.middleware` records each model response (V3 usage,
184
+ the tool calls asked for) and keeps each call's raw arguments by `toolCallId` for the decision. `tk.toolApproval` maps
185
+ `deny` to `denied` and `ask` to `user-approval`; without it an `ask` is refused. `tk.tools()` wraps each `execute`, so a
186
+ forged or replayed `tool-approval-response` runs nothing. `experimental_toolApprovalSecret` (the SDK's HMAC over its own
187
+ approval requests) may be set too; it does not replace the signer's check. Spans from `@ai-sdk/otel` are telemetry, not
188
+ evidence: the signed records are the middleware's and the adapter's. Tools without `execute` and provider-executed tools
189
+ are not gated; they appear in the recorded model responses.
190
+
191
+ MCP tools (`@ai-sdk/mcp`): `tk.tools(await client.tools(), { mcp: "filesystem" })` decides each as
192
+ `mcp:filesystem/<tool>`. Your own `toolApproval` (a function, or one per tool name) composes with the signer's:
193
+ `toolApproval: tk.toolApprovalWith(mine)` asks the policy first, and your answer applies only to a call the policy
194
+ allows (a call you deny is recorded as refused; one you answer `user-approval` and then deny in your own UI stays
195
+ decided and not completed); `tk.toolApproval` alone replaces yours.
196
+
197
+ **Claude Agent SDK** (`@cygnuxlabs/tracekit/v2/claude-agent-sdk`)
198
+
199
+ ```ts
200
+ import { query } from "@anthropic-ai/claude-agent-sdk";
201
+ import { tracekitHooks, tracekitSessionStore } from "@cygnuxlabs/tracekit/v2/claude-agent-sdk";
202
+ const run = await new Client().registerRun("my-agent");
203
+ query({ prompt, options: { hooks: tracekitHooks(run), sessionStore: tracekitSessionStore(myStore, run) } });
204
+ ```
205
+
206
+ The gate is the `PreToolUse` hook (it runs for every call; `canUseTool` is skipped for calls the CLI's permission mode
207
+ allows). An `ask` is held in the hook for up to 540 s, below the hook timeout it sets (600 s); a call not approved by
208
+ then is blocked. `PostToolUse`/`PostToolUseFailure` complete the call, `SessionEnd` closes the run. The session store
209
+ wrapper commits each saved transcript as a `state_write` chained from the last one, so a transcript edited in the store
210
+ between two writes shows up as a signed `state_tamper` gap.
211
+
212
+ ## Migrating from v1
213
+
214
+ The v1 SDK (`Tracekit`, driving `python -m tracekit.bridge`) is deprecated and removed in 0.5.0. Until then it stays at
215
+ `@cygnuxlabs/tracekit/v1`; `Tracekit.start` warns once per process (`DeprecationWarning`, code `TRACEKIT_V1`) and the bridge
216
+ prints a matching notice on stderr; `TRACEKIT_NO_DEPRECATION=1` silences both. The import becomes
217
+ `import { Tracekit } from "@cygnuxlabs/tracekit/v1"`.
218
+
219
+ | v1 (`@cygnuxlabs/tracekit/v1`) | v2 (`@cygnuxlabs/tracekit`) |
220
+ |---|---|
221
+ | `await Tracekit.start({ agent })` | `const client = new Client(); const run = await client.registerRun(agent)` |
222
+ | `await tk.tool(name, args, fn, { toolUseId })` | `await run.decide(toolUseId, name, args)`, run `fn` on `allow`, then `await run.complete(toolUseId, "ok", { result })` |
223
+ | `TracekitDenied` | `d.decision === "deny"` (`d.rule_ids`); a refusal by the signer rejects with `RPCError` |
224
+ | held for approval inside `tk.tool` | `d.decision === "ask"`: `run.approvalRequest(id)`, then `run.approvalWait(approval_id)` |
225
+ | `instrumentOpenAI` / `instrumentAnthropic`, `tk.modelBegin` / `tk.modelEnd` | `run.modelEvent(provider, model, "response", { stop_reason, usage, tool_uses })`, or a framework adapter below |
226
+ | `tracekitMiddleware(tk)` | `tracekitAI(run).middleware` from `@cygnuxlabs/tracekit/v2/vercel-ai`, with `tools` and `toolApproval` |
227
+ | `tk.prompt` / `tk.say` / `tk.think` | not recorded by the v2 signer |
228
+ | `tk.end(reason)` | `await run.close(reason); await client.close()` |
229
+
230
+ ```ts
231
+ // v1
232
+ import { Tracekit, TracekitDenied } from "@cygnuxlabs/tracekit/v1";
233
+ const tk = await Tracekit.start({ agent: "research-bot" });
234
+ try {
235
+ out = await tk.tool("Bash", args, () => runShell(args), { toolUseId: call.id });
236
+ } catch (e) {
237
+ if (!(e instanceof TracekitDenied)) throw e;
238
+ }
239
+ await tk.end();
240
+
241
+ // v2
242
+ import { Client } from "@cygnuxlabs/tracekit";
243
+ const client = new Client();
244
+ const run = await client.registerRun("research-bot");
245
+ const d = await run.decide(call.id, "Bash", call.function.arguments);
246
+ if (d.decision === "allow") {
247
+ out = await runShell(args);
248
+ await run.complete(call.id, "ok", { result: out });
249
+ }
250
+ await run.close();
251
+ await client.close();
252
+ ```
253
+
254
+ ## v1 SDK (deprecated, `@cygnuxlabs/tracekit/v1`)
255
+
256
+ Removed in 0.5.0; see the migration above. Tool calls pass Tracekit's policy gate before they run, and model calls
257
+ become signed evidence, in the same ledger and format as the Python SDK.
258
+
259
+ ```ts
260
+ import OpenAI from "openai";
261
+ import { Tracekit, TracekitDenied, instrumentOpenAI } from "@cygnuxlabs/tracekit/v1";
262
+
263
+ const tk = await Tracekit.start({ agent: "research-bot" });
264
+ const openai = instrumentOpenAI(new OpenAI(), tk); // also: instrumentAnthropic(client, tk), tracekitMiddleware(tk) for the Vercel AI SDK
265
+
266
+ const r = await openai.chat.completions.create({ model: "gpt-4o", messages, tools });
267
+ for (const call of r.choices[0].message.tool_calls ?? []) {
268
+ await tk.tool("Bash", JSON.parse(call.function.arguments), () => runShell(call), { toolUseId: call.id }); // TracekitDenied if blocked
269
+ }
270
+ await tk.end();
271
+ ```
272
+
273
+ - `tk.tool(name, args, fn)`: the policy decision is made and signed before `fn` runs; a denied (or held, not approved)
274
+ call throws `TracekitDenied` and `fn` never runs. Pass the model's call id as `toolUseId` to link request and execution.
275
+ - `instrumentOpenAI` (chat completions, Responses), `instrumentAnthropic` (messages): a signed request event before the
276
+ call is sent, and a response event with model, finish reason, requested tool calls, token usage, error, status and
277
+ time to first chunk. Streams are recorded when they end, fail or are abandoned: the SDK replaces the stream's async
278
+ iterator in place with a recording one; non-streamed responses come back as the provider SDK returned them. The
279
+ wrapped `create` is an async function, so it returns a plain Promise: helpers on the SDK's own promise object (such
280
+ as `.withResponse()`) are not available through it. A provider error is recorded and rethrown. The request event is
281
+ written before the call is sent: if the bridge reports an error or times out, that error is thrown and the call is not sent; if the
282
+ bridge has exited, `fail_mode: closed` throws `TracekitDenied` and `fail_mode: open` sends the call unrecorded with a
283
+ warning on stderr. A failure to record a call that already completed is written to stderr and counted in
284
+ `tk.recordFailures`; it never replaces the call's result.
285
+ - `tracekitMiddleware(tk)`: `wrapLanguageModel({ model, middleware: tracekitMiddleware(tk) })` in the Vercel AI SDK
286
+ (v4 and v5 result shapes).
287
+
288
+ **How it works.** The SDK starts `python -m tracekit.bridge` and talks to it over stdio, so policy evaluation, secret
289
+ redaction, the signer client and the event format are Tracekit's own Python code, not a second implementation. It needs
290
+ the `tracekit` Python package and a signer (`tracekit init --dev`); set `TRACEKIT_PYTHON` to pick the interpreter.
291
+
292
+ Tests (`npm test`): `test/sdk.test.mjs` runs the v1 SDK with the real OpenAI and Anthropic SDKs against a mocked fetch and a real signer, then verify the
293
+ bundle; `test/adapters.test.mjs` runs the adapter contract for each adapter, through small stand-ins for the
294
+ frameworks' hook and middleware interfaces, against an in-test fake signer and (with Python and the signer extra) the
295
+ real one.
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ // `npx @cygnuxlabs/tracekit up|down|status|view|verify|signer ...`: runs the Tracekit CLI that src/v2/launcher.ts finds.
3
+ // `verify BUNDLE --trust trust.json` (a v2 bundle) runs the TypeScript verifier instead, so it needs nothing else,
4
+ // unless an option only the Python verifier has is given.
5
+ import { spawnSync } from "node:child_process";
6
+ import { INSTALL_HINT, signerCommand, signerEnv } from "../dist/v2/launcher.js";
7
+
8
+ const args = process.argv.slice(2);
9
+ // the options only the Python verifier has (monitor reports, revocations, the v1 bridge) still go to it
10
+ const PYTHON_ONLY = ["--monitor-report", "--revocations", "--v1-ledger", "--v1-key"];
11
+ if (args[0] === "verify" && args.includes("--trust") && !args.some((a) => PYTHON_ONLY.includes(a.split("=")[0]))) {
12
+ const [major, minor] = process.versions.node.split(".").map(Number);
13
+ if (major < 20 || major === 20 && minor < 12) { // the verifier's JSON imports and deflate-raw
14
+ console.error(`tracekit verify: the TypeScript verifier needs Node >= 20.12 (this is ${process.versions.node})`);
15
+ process.exit(2);
16
+ }
17
+ const { main } = await import("../dist/verify/cli.js");
18
+ process.exit(await main(args.slice(1)));
19
+ }
20
+ const cmd = signerCommand();
21
+ if (!cmd) {
22
+ console.error(`tracekit: ${INSTALL_HINT}`);
23
+ process.exit(1);
24
+ }
25
+ const r = spawnSync(cmd[0], [...cmd.slice(1), ...args], { stdio: "inherit", env: signerEnv(cmd) });
26
+ if (r.error) {
27
+ console.error(`tracekit: ${cmd[0]}: ${r.error.message}`);
28
+ process.exit(1);
29
+ }
30
+ process.exit(r.status ?? 1);
package/dist/v1.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ export declare class TracekitDenied extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface StartOptions {
5
+ agent?: string;
6
+ sessionId?: string;
7
+ cwd?: string;
8
+ python?: string;
9
+ env?: Record<string, string>;
10
+ /** Per-request timeout for the bridge (default 30 s). */
11
+ timeoutMs?: number;
12
+ /** Timeout for a tool call held for approval (default 61 min: the signer caps an approval at one hour). */
13
+ approvalTimeoutMs?: number;
14
+ }
15
+ export interface Usage {
16
+ input_tokens: number;
17
+ output_tokens: number;
18
+ cache_read_tokens?: number | null;
19
+ cache_write_tokens?: number | null;
20
+ reasoning_tokens?: number | null;
21
+ }
22
+ export interface ModelEnd {
23
+ response?: unknown;
24
+ error?: string | null;
25
+ status?: number | null;
26
+ stop_reason?: string | null;
27
+ tool_uses?: {
28
+ id: string;
29
+ name: string;
30
+ }[];
31
+ usage?: Usage | null;
32
+ model?: string | null;
33
+ first_byte_ms?: number | null;
34
+ }
35
+ export declare class Tracekit {
36
+ private bridge;
37
+ private run;
38
+ private failMode;
39
+ private timeoutMs;
40
+ private approvalTimeoutMs;
41
+ readonly sessionId: string;
42
+ /** Recording failures after a wrapped call had already succeeded: reported on stderr, never thrown. */
43
+ recordFailures: number;
44
+ private warned;
45
+ private constructor();
46
+ /** Start a run. One Tracekit per run; call end() when the agent is done. */
47
+ static start(opts?: StartOptions): Promise<Tracekit>;
48
+ /**
49
+ * One bridge request. If the bridge has exited, the run's fail mode decides: open returns null (the caller proceeds
50
+ * unrecorded, with a warning), closed refuses with TracekitDenied.
51
+ */
52
+ private call;
53
+ /**
54
+ * A record that gates nothing (model_begin, prompt, say, think): under fail_mode=open a timeout or bridge error lets
55
+ * the caller proceed unrecorded, as the Python SDK does. tool_begin never goes through here: a timeout there can be
56
+ * an unanswered approval hold.
57
+ */
58
+ private record;
59
+ private warnOnce;
60
+ /** Record after the wrapped call succeeded: a failure here is reported, never thrown over the call's result. */
61
+ settle(p: Promise<unknown>): Promise<void>;
62
+ prompt(text: string): Promise<void>;
63
+ say(text: string): Promise<void>;
64
+ think(text: string): Promise<void>;
65
+ /**
66
+ * Run a tool through the policy gate. The policy decision is made and signed before `fn` runs; a denied (or held and
67
+ * not approved) call throws TracekitDenied and `fn` never runs. The result or error is recorded afterwards.
68
+ * Pass the model's tool-call id as toolUseId to link the execution to the model request that asked for it.
69
+ */
70
+ tool<T>(name: string, args: Record<string, unknown>, fn: () => T | Promise<T>, opts?: {
71
+ toolUseId?: string;
72
+ }): Promise<T>;
73
+ /** Low level: record a model call yourself. begin() is written before the request is sent. Null: not recorded. */
74
+ modelBegin(provider: string, operation: string, model: string | null, request: unknown, streamed?: boolean): Promise<string | null>;
75
+ modelEnd(exchange: string | null, end: ModelEnd): Promise<void>;
76
+ end(reason?: string): Promise<void>;
77
+ }
78
+ export declare function usageFromOpenAI(u: any): Usage | null;
79
+ export declare function usageFromAnthropic(u: any): Usage | null;
80
+ /** OpenAI SDK: chat.completions.create and responses.create, streaming included. Returns the same client. */
81
+ export declare function instrumentOpenAI<C extends Record<string, any>>(client: C, tk: Tracekit): C;
82
+ /** Anthropic SDK: messages.create, streaming (stream: true) included. Returns the same client. */
83
+ export declare function instrumentAnthropic<C extends Record<string, any>>(client: C, tk: Tracekit): C;
84
+ /**
85
+ * Vercel AI SDK language-model middleware (use with wrapLanguageModel({ model, middleware: tracekitMiddleware(tk) })).
86
+ * Works with the v4 (promptTokens/toolCalls) and v5 (inputTokens/content parts) result shapes.
87
+ */
88
+ export declare function tracekitMiddleware(tk: Tracekit, provider?: string): {
89
+ wrapGenerate: ({ doGenerate, params, model }: any) => Promise<any>;
90
+ wrapStream: ({ doStream, params, model }: any) => Promise<any>;
91
+ };
92
+ /**
93
+ * Stagehand: wrap page/stagehand methods (act, extract, observe, goto by default) so every browser action is a
94
+ * policy-checked, signed `browser:<method>` step. A denied action throws TracekitDenied and never reaches the browser.
95
+ * Works on any object with those methods (Stagehand v2 `page`, or the v3 `stagehand` instance). Returns the same object.
96
+ */
97
+ export declare function instrumentStagehand<P extends Record<string, any>>(page: P, tk: Tracekit, methods?: string[]): P;