@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.
- package/README.md +294 -2
- package/bin/tracekit.mjs +30 -0
- package/dist/v1.d.ts +97 -0
- package/dist/v1.js +535 -0
- package/dist/v2/adapters/claude-agent-sdk.d.ts +45 -0
- package/dist/v2/adapters/claude-agent-sdk.js +142 -0
- package/dist/v2/adapters/gate.d.ts +62 -0
- package/dist/v2/adapters/gate.js +195 -0
- package/dist/v2/adapters/openai-agents.d.ts +85 -0
- package/dist/v2/adapters/openai-agents.js +114 -0
- package/dist/v2/adapters/vercel-ai.d.ts +51 -0
- package/dist/v2/adapters/vercel-ai.js +132 -0
- package/dist/v2/client.d.ts +159 -0
- package/dist/v2/client.js +749 -0
- package/dist/v2/executor.d.ts +49 -0
- package/dist/v2/executor.js +57 -0
- package/dist/v2/jcs.d.ts +5 -0
- package/dist/v2/jcs.js +12 -0
- package/dist/v2/launcher.d.ts +10 -0
- package/dist/v2/launcher.js +50 -0
- package/dist/v2/rpc_schema.d.ts +3 -0
- package/dist/v2/rpc_schema.js +43 -0
- package/dist/v2/validate.d.ts +6 -0
- package/dist/v2/validate.js +70 -0
- package/dist/verify/cli.d.ts +1 -0
- package/dist/verify/cli.js +46 -0
- package/dist/verify/detect.d.ts +41 -0
- package/dist/verify/detect.js +289 -0
- package/dist/verify/index.d.ts +34 -0
- package/dist/verify/index.js +930 -0
- package/dist/verify/jcs.d.ts +19 -0
- package/dist/verify/jcs.js +154 -0
- package/dist/verify/primitives.d.ts +62 -0
- package/dist/verify/primitives.js +298 -0
- package/dist/verify/schema.d.ts +11 -0
- package/dist/verify/schema.js +98 -0
- package/dist/verify/tracekit.event.v2.json +4588 -0
- package/dist/verify/zip.d.ts +17 -0
- package/dist/verify/zip.js +145 -0
- package/package.json +42 -4
package/README.md
CHANGED
|
@@ -1,3 +1,295 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cygnuxlabs/tracekit
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/bin/tracekit.mjs
ADDED
|
@@ -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;
|