@cygnuxlabs/tracekit

中文文档
1.6.0发布于 2026/11/09MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 2163

Tracekit for TypeScript/JavaScript agents: a native client of the Tracekit signer: policy-gated tool calls and signed evidence, no runtime dependencies.

原始使用文档
文档目录6 节
  1. Install
  2. Signer client ( @cygnuxlabs/tracekit )
  3. Verify a bundle ( @cygnuxlabs/tracekit/verify )
  4. v2 framework adapters
  5. Migrating from v1
  6. v1 SDK (deprecated, @cygnuxlabs/tracekit/v1 )

@cygnuxlabs/tracekit

Experimental: the client and its Vercel AI SDK integration work and are tested, but may change and get less hardening than the stable core. Its verifier (src/verify/) is stable.

Tracekit for TypeScript and JavaScript agents: every tool call is decided by the Tracekit signer before it runs and recorded after, and model responses become signed evidence. The client talks to the signer directly; it needs no Python at runtime (the same-user dev signer is started with Python when none is running).

Install

Node 18 or newer:

npm install @cygnuxlabs/tracekit

npm also installs the signer package for your platform (@cygnuxlabs/tracekit-signer-<platform>: Apple Silicon macOS, Linux x64 and arm64 with glibc, Windows x64), which bundles a Python, so npx @cygnuxlabs/tracekit up starts a dev signer without one. On an Intel Mac or Alpine there is none: pip install tracekit-ai.

Or build it from a checkout and install the file it makes:

cd sdk/typescript && npm ci && npm run build && npm pack    # writes cygnuxlabs-tracekit-<version>.tgz
cd /path/to/your/app && npm install /path/to/Tracekit/sdk/typescript/cygnuxlabs-tracekit-*.tgz

Signer client (@cygnuxlabs/tracekit)

A native client of the v2 signer RPC with no runtime dependencies (Node ≥ 18 built-ins only, no Python bridge); also exported as @cygnuxlabs/tracekit/v2.

import { Client, withRun, currentRun } from "@cygnuxlabs/tracekit";

const client = new Client();                              // or new Client({ signer: "https://signer:8443" })
const run = await client.registerRun("research-bot");
const d = await run.decide(call.id, "Bash", call.function.arguments);   // the raw model string, or a parsed value
if (d.decision === "allow") {
  if ((await run.approvalConsume(call.id, "Bash", call.function.arguments)).ok)   // never throws: ok false means it doesn't run
    await run.complete(call.id, "ok", { result: await runShell(call) });   // never throws: a recording failure is a warning
} else if (d.decision === "ask") {
  const { approval_id } = await run.approvalRequest(call.id);
  const { state } = await run.approvalWait(approval_id, 300_000);   // on a connection of its own
  if (state === "approved" && (await run.approvalConsume(call.id, "Bash", call.function.arguments, approval_id)).ok)
    await run.complete(call.id, "ok", { result: await runShell(call) });
}
await run.close();
await client.close();                                     // waits for what is in flight
  • Signer: $TRACEKIT_SIGNER names a Unix socket, tcp://127.0.0.1:port (with $TRACEKIT_SIGNER_TOKEN; both sides prove they hold it, it never goes on the wire) or https://host:port ($TRACEKIT_SIGNER_TOKEN_FILE: a bearer token re-read per call; $TRACEKIT_SIGNER_CERT/_KEY: mTLS; $TRACEKIT_SIGNER_CA: the signer's CA). In system mode the signer named by the root-owned /etc/tracekit/client.json is used and a different $TRACEKIT_SIGNER is refused. Otherwise the same-user dev signer is found, or started with tracekit up --json: $TRACEKIT_PYTHON -m tracekit if set, else a pip-installed tracekit on PATH, else the Python bundled in this platform's @cygnuxlabs/tracekit-signer-<platform> package. npx @cygnuxlabs/tracekit up|down|status|view|verify|signer runs the same command.
  • Protocol: hello first; a signer that does not speak this client's RPC version is refused with Incompatible. Requests are validated against the RPC schemas before they are sent, pipelined on one connection and answered in order; a call whose connection drops is resent with the same request_id. Each handle's events carry the client's stream and a client_seq per run.
  • Fail modes: only when the signer cannot be reached does decide return the run's fail mode for the tool class (fields.tool_class_hint, from register_run's fail_modes, default closed) with unavailable: true. A refusal from the signer rejects with RPCError, never an allow.
  • Serverless: client.waitUntil(promise) and await client.flush() before the host freezes the process.
  • withRun(run, fn) makes run the currentRun() of everything fn awaits (AsyncLocalStorage).
  • argsDigest(tool, args), canonicalize, strictParse: JCS (RFC 8785) and the strict parser, byte-identical to the Python client on the shared vectors in tests/vectors/jcs.jsonl.

Verify a bundle (@cygnuxlabs/tracekit/verify)

An independent verifier of v2 bundles (.tkb), written from docs/format-v2.md and tested to agree with the Python verifier. No dependencies: Web Crypto and DecompressionStream, so Node ≥ 20.12 and current browsers.

npx @cygnuxlabs/tracekit verify run.tkb --trust trust.json [--json] [--strict]

Exit 0 verified, 1 failed, 2 unusable or unverifiable, 3 a warning under --strict; the report and --json are those of tracekit verify. In code, Node or a browser:

import { verify, formatReport } from "@cygnuxlabs/tracekit/verify";

const bundle = new Uint8Array(await (await fetch("run.tkb")).arrayBuffer());
const trust = new Uint8Array(await (await fetch("trust.json")).arrayBuffer());   // your own pinned trust config
const { report, code } = await verify(bundle, trust);
console.log(report.integrity, report.assurance);   // e.g. "VERIFIED", "witnessed; records ed25519; ..."

It does not check SLH-DSA checkpoint lines, Rekor anchors or certified record keys: a bundle that relies on one is UNVERIFIABLE (not checked by this verifier: ...), never VERIFIED. Monitor reports, revocations and the v1 format bridge need the Python verifier.

verify(bundle, null), with no trust config, runs every check that needs no pinned key and is never VERIFIED: a changed record still fails, an intact bundle is UNVERIFIABLE (no trust config: ...).

In a browser: dist/verify.html in this package (and a release asset) is this verifier as one self-contained page. Drop in a bundle and its trust config; its Content Security Policy lets it load and send nothing, so neither file leaves the browser (verify in your browser). make verify-page builds it from a checkout.

v2 framework adapters

One call registers a run (the dev signer is started if none answers), closes it when the process is about to exit, and hands you the adapters bound to it; then tracekit last exports, pins and verifies the run:

import { instrument } from "@cygnuxlabs/tracekit";
const tk = await instrument();                    // the same on a second call
const model = wrapLanguageModel({ model: openai("gpt-5"), middleware: tk.ai.middleware });   // Vercel AI SDK
const r = await generateText({ model, tools: tk.ai.tools({ pay }), toolApproval: tk.ai.toolApproval, messages });
const agent = new Agent({ name: "payer", tools: [tk.agents.tool(tool, { name: "pay", description, parameters, execute })] });   // OpenAI Agents JS

ES modules can't be patched from outside, so unlike Python's tracekit.instrument() the adapters are passed to the framework by you.

Each adapter gates every tool call through the signer: it passes the call id, its attempt and the model's raw arguments, the signer decides, an ask waits for a person (held in the hook where the framework allows, else paused and resumed), and every call, allowed or approved, runs only after the signer's approval_consume agrees. A deny reaches the model as the tool's error result and the run goes on. The run's fail mode (register_run's fail_modes) applies only when the signer cannot be reached at the decision; a refusal, or a failure while waiting for an approval, never lets the call run. Recording a finished call never throws and never changes its result (a failure is a TracekitWarning). The frameworks are optional peer dependencies: the adapters import none of them, and the client keeps no runtime dependencies. No adapter wraps a provider client, so the SDKs' own APIPromise (.withResponse()) and streams (messages.stream(), streamText) work as without Tracekit. Offline examples: examples/v2_*.mjs, and a Mastra workflow that suspends on a held call and resumes with its approval id.

  • A server: a run per conversation. Register a run per conversation and make it current around each request with withRun(run, () => runner.run(...)): every adapter decides the calls made inside it in that run (its complete and approvals follow the call), and uses the run it was built with otherwise. instrument()'s own run is replaced by a new one (a warning, linked as parent_run while the old one is closing) when the signer closed it after it was idle.
  • Exit. instrument() closes its run on beforeExit, and on SIGINT or SIGTERM when nothing else handles the signal (then the signal's default action runs). A signal your app or a library handles is left to it (@openai/agents exits on SIGINT/SIGTERM): close the run in your own handler (await tk.run.close()) before you exit. A run stays open while approvals of it wait for a person or, approved, to be resumed. To approve and resume in another process, keep the run's runId and runToken with the framework's saved state and re-attach: new RunHandle(new Client(), { run_id, run_token }); close that client when you are done with it (run.client.close()), or re-attach every run through one shared client. process.exit() skips all of this: close first. The offline Vercel AI resume example saves the run and messages, prints the approval command, and resumes in a second process.
  • The LLM gateway. Point the provider at it with the run in a header, e.g. OpenAI Agents setDefaultOpenAIClient(new OpenAI({ baseURL, apiKey: <gateway token>, defaultHeaders: { "X-Tracekit-Run": ${run.runId}.${run.runToken} } })), Vercel AI createOpenAI({ baseURL, apiKey, headers }). Register the run as the identity the gateway relays (TRACEKIT_SIGNER=https://… with the same token), and grant that identity the RPCs the adapters use, approval_list included (limits). The adapters' own model events are then optional: the gateway records each exchange too.

OpenAI Agents SDK (@cygnuxlabs/tracekit/v2/openai-agents)

import { Agent, Runner, tool } from "@openai/agents";
import { TracekitAgents } from "@cygnuxlabs/tracekit/v2/openai-agents";
const tk = new TracekitAgents(await new Client().registerRun("payer"));
const agent = new Agent({ name: "payer", tools: [tk.tool(tool, { name: "pay", description, parameters, execute })] });
let result = await runner.run(agent, "pay acct-42 $15", { context: {} });   // the context must be an object
if (result.interruptions.length) {                                         // an `ask`: once decided in the signer
  await tk.applyDecisions(result.state);
  result = await runner.run(agent, result.state);
}
await tk.recordResponses(result.rawResponses, "gpt-5");

tk.tool(tool, options) builds the tool with the SDK's tool(): the signer's policy replaces needsApproval, and a tool input guardrail (after your own) gates the call on its raw arguments. The approval id travels in the RunState only as a hint (context.tracekit.approvals); the signer checks it. Leave toolExecution.preApprovalInputGuardrails off: with it on, the guardrail runs before the interruption and every ask is refused at once.

A local MCP server (MCPServerStdio, SSE, streamable HTTP) is gated through its tool guardrails: new MCPServerStdio({ name: "filesystem", ..., ...tk.mcp("filesystem") }) decides each call as mcp:filesystem/<tool>, holds an ask in the guardrail until a person decides, and completes the call with the server's output. Pass the server's own name: with mcpConfig.includeServerInToolNames the model's mcp_<server>__<tool> is turned back into mcp:<server>/<tool> from it. A call your own input guardrails refuse is recorded as refused. Without it, local MCP calls are not gated. A sub-agent used as a tool (agent.asTool()) is not gated either, and its model responses are not in the outer rawResponses: gate its own tools with tk.tool, and record its responses (customOutputExtractor gets the nested result) or run the LLM gateway. Also not gated, listed as uncovered: hosted tools (web and file search, code interpreter, hosted MCP, hosted shell), shell, apply_patch and computer tools, and handoffs (name them in the policy's handoffs: so they reconcile). recordResponses records them (T3) in a signed model event per response.

Vercel AI SDK v7 (@cygnuxlabs/tracekit/v2/vercel-ai)

import { generateText, wrapLanguageModel } from "ai";
import { tracekitAI } from "@cygnuxlabs/tracekit/v2/vercel-ai";
const tk = tracekitAI(await new Client().registerRun("assistant"));
const model = wrapLanguageModel({ model: openai("gpt-5"), middleware: tk.middleware });
const r = await generateText({ model, tools: tk.tools({ pay }), toolApproval: tk.toolApproval, messages });
// asks end the step with tool-approval-request parts; once decided in the signer:
messages.push(...r.response.messages, { role: "tool", content: await tk.approvalResponses(r.content) });

The same options work for streamText and ToolLoopAgent. tk.middleware records each model response (V3 usage, the tool calls asked for) and keeps each call's raw arguments by toolCallId for the decision. tk.toolApproval maps deny to denied and ask to user-approval; without it an ask is refused. tk.tools() wraps each execute, so a forged or replayed tool-approval-response runs nothing. experimental_toolApprovalSecret (the SDK's HMAC over its own approval requests) may be set too; it does not replace the signer's check. Spans from @ai-sdk/otel are telemetry, not evidence: the signed records are the middleware's and the adapter's. Tools without execute and provider-executed tools are not gated; they appear in the recorded model responses.

MCP tools (@ai-sdk/mcp): tk.tools(await client.tools(), { mcp: "filesystem" }) decides each as mcp:filesystem/<tool>. Your own toolApproval (a function, or one per tool name) composes with the signer's: toolApproval: tk.toolApprovalWith(mine) asks the policy first, and your answer applies only to a call the policy allows (a call you deny is recorded as refused; one you answer user-approval and then deny in your own UI stays decided and not completed); tk.toolApproval alone replaces yours.

Claude Agent SDK (@cygnuxlabs/tracekit/v2/claude-agent-sdk)

import { query } from "@anthropic-ai/claude-agent-sdk";
import { tracekitHooks, tracekitSessionStore } from "@cygnuxlabs/tracekit/v2/claude-agent-sdk";
const run = await new Client().registerRun("my-agent");
query({ prompt, options: { hooks: tracekitHooks(run), sessionStore: tracekitSessionStore(myStore, run) } });

The gate is the PreToolUse hook (it runs for every call; canUseTool is skipped for calls the CLI's permission mode 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 then is blocked. PostToolUse/PostToolUseFailure complete the call, SessionEnd closes the run. The session store wrapper commits each saved transcript as a state_write chained from the last one, so a transcript edited in the store between two writes shows up as a signed state_tamper gap.

Migrating from v1

The v1 SDK (Tracekit, driving python -m tracekit.bridge) is deprecated and removed in 0.5.0. Until then it stays at @cygnuxlabs/tracekit/v1; Tracekit.start warns once per process (DeprecationWarning, code TRACEKIT_V1) and the bridge prints a matching notice on stderr; TRACEKIT_NO_DEPRECATION=1 silences both. The import becomes import { Tracekit } from "@cygnuxlabs/tracekit/v1".

v1 (@cygnuxlabs/tracekit/v1)v2 (@cygnuxlabs/tracekit)
await Tracekit.start({ agent })const client = new Client(); const run = await client.registerRun(agent)
await tk.tool(name, args, fn, { toolUseId })await run.decide(toolUseId, name, args), run fn on allow, then await run.complete(toolUseId, "ok", { result })
TracekitDeniedd.decision === "deny" (d.rule_ids); a refusal by the signer rejects with RPCError
held for approval inside tk.toold.decision === "ask": run.approvalRequest(id), then run.approvalWait(approval_id)
instrumentOpenAI / instrumentAnthropic, tk.modelBegin / tk.modelEndrun.modelEvent(provider, model, "response", { stop_reason, usage, tool_uses }), or a framework adapter below
tracekitMiddleware(tk)tracekitAI(run).middleware from @cygnuxlabs/tracekit/v2/vercel-ai, with tools and toolApproval
tk.prompt / tk.say / tk.thinknot recorded by the v2 signer
tk.end(reason)await run.close(reason); await client.close()
// v1
import { Tracekit, TracekitDenied } from "@cygnuxlabs/tracekit/v1";
const tk = await Tracekit.start({ agent: "research-bot" });
try {
  out = await tk.tool("Bash", args, () => runShell(args), { toolUseId: call.id });
} catch (e) {
  if (!(e instanceof TracekitDenied)) throw e;
}
await tk.end();

// v2
import { Client } from "@cygnuxlabs/tracekit";
const client = new Client();
const run = await client.registerRun("research-bot");
const d = await run.decide(call.id, "Bash", call.function.arguments);
if (d.decision === "allow") {
  out = await runShell(args);
  await run.complete(call.id, "ok", { result: out });
}
await run.close();
await client.close();

v1 SDK (deprecated, @cygnuxlabs/tracekit/v1)

Removed in 0.5.0; see the migration above. Tool calls pass Tracekit's policy gate before they run, and model calls become signed evidence, in the same ledger and format as the Python SDK.

import OpenAI from "openai";
import { Tracekit, TracekitDenied, instrumentOpenAI } from "@cygnuxlabs/tracekit/v1";

const tk = await Tracekit.start({ agent: "research-bot" });
const openai = instrumentOpenAI(new OpenAI(), tk);        // also: instrumentAnthropic(client, tk), tracekitMiddleware(tk) for the Vercel AI SDK

const r = await openai.chat.completions.create({ model: "gpt-4o", messages, tools });
for (const call of r.choices[0].message.tool_calls ?? []) {
  await tk.tool("Bash", JSON.parse(call.function.arguments), () => runShell(call), { toolUseId: call.id });  // TracekitDenied if blocked
}
await tk.end();
  • tk.tool(name, args, fn): the policy decision is made and signed before fn runs; a denied (or held, not approved) call throws TracekitDenied and fn never runs. Pass the model's call id as toolUseId to link request and execution.
  • instrumentOpenAI (chat completions, Responses), instrumentAnthropic (messages): a signed request event before the call is sent, and a response event with model, finish reason, requested tool calls, token usage, error, status and time to first chunk. Streams are recorded when they end, fail or are abandoned: the SDK replaces the stream's async iterator in place with a recording one; non-streamed responses come back as the provider SDK returned them. The wrapped create is an async function, so it returns a plain Promise: helpers on the SDK's own promise object (such as .withResponse()) are not available through it. A provider error is recorded and rethrown. The request event is 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 bridge has exited, fail_mode: closed throws TracekitDenied and fail_mode: open sends the call unrecorded with a warning on stderr. A failure to record a call that already completed is written to stderr and counted in tk.recordFailures; it never replaces the call's result.
  • tracekitMiddleware(tk): wrapLanguageModel({ model, middleware: tracekitMiddleware(tk) }) in the Vercel AI SDK (v4 and v5 result shapes).

How it works. The SDK starts python -m tracekit.bridge and talks to it over stdio, so policy evaluation, secret redaction, the signer client and the event format are Tracekit's own Python code, not a second implementation. It needs the tracekit Python package and a signer (tracekit init --dev); set TRACEKIT_PYTHON to pick the interpreter.

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 bundle; test/adapters.test.mjs runs the adapter contract for each adapter, through small stand-ins for the frameworks' hook and middleware interfaces, against an in-test fake signer and (with Python and the signer extra) the real one.

版本与发布记录

@cygnuxlabs/tracekit 共发布 13 个版本,首次发布于 2026/10/17;过去 12 个月发布了 13 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.6.0latest2026/11/094 天前
1.5.32026/11/049 天前
1.5.22026/10/3113 天前
1.5.12026/10/3014 天前
1.5.02026/10/2816 天前
1.4.22026/10/2420 天前

关于 @cygnuxlabs/tracekit 的常见问题

@cygnuxlabs/tracekit 是什么?

Tracekit for TypeScript/JavaScript agents: a native client of the Tracekit signer: policy-gated tool calls and signed evidence, no runtime dependencies.

如何安装 @cygnuxlabs/tracekit?

在项目目录运行 npm install @cygnuxlabs/tracekit,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 tracekit,可以用 npx tracekit 直接运行。

@cygnuxlabs/tracekit 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cygnuxlabs/tracekit 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cygnuxlabs/tracekit 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cygnuxlabs/tracekit 可以商用吗?

@cygnuxlabs/tracekit 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cygnuxlabs/tracekit 的下载量有多少?

根据 npm 官方下载统计,@cygnuxlabs/tracekit 在 2026/10/11 – 2026/11/09 这 30 天内被下载 2,163 次,日均约 72 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cygnuxlabs/tracekit 还在维护吗?

最新版本 1.6.0 发布于 2026/11/09(4 天前);至今共发布 13 个版本,其中过去 12 个月发布了 13 个;首次发布于 2026/10/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。