@cygnuxlabs/tracekit 0.0.0-stage → 1.2.2
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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { digest } from "../jcs.js";
|
|
2
|
+
import { EXECUTOR } from "../executor.js";
|
|
3
|
+
import { BLOCKED, Gate, approvalStates, defined, failure, keep, recordResponse, refusal, take, toolUse } from "./gate.js";
|
|
4
|
+
/** LanguageModelV3/V4 usage as the signer's usage buckets (input_tokens without cache reads). */
|
|
5
|
+
function usage(u) {
|
|
6
|
+
return u?.inputTokens && defined({ input_tokens: u.inputTokens.noCache ?? u.inputTokens.total, output_tokens: u.outputTokens?.total,
|
|
7
|
+
cache_read_tokens: u.inputTokens.cacheRead, cache_write_tokens: u.inputTokens.cacheWrite, reasoning_tokens: u.outputTokens?.reasoning });
|
|
8
|
+
}
|
|
9
|
+
const stop = (fr) => (typeof fr === "string" ? fr : fr?.unified);
|
|
10
|
+
export function tracekitAI(run) {
|
|
11
|
+
const gate = new Gate(run);
|
|
12
|
+
const executors = new Set(); // the names of tool executor tools tk.tools() was given
|
|
13
|
+
const raw = new Map(); // toolCallId -> the model's arguments string
|
|
14
|
+
const decided = new Map(); // toolCallId -> its decision, from toolApproval to execute
|
|
15
|
+
const named = new Map(); // tool name -> the name it is decided as (mcp:<server>/<tool>)
|
|
16
|
+
const seen = (p) => {
|
|
17
|
+
if (p?.type === "tool-call" && !p.providerExecuted && typeof p.input === "string")
|
|
18
|
+
keep(raw, p.toolCallId, p.input);
|
|
19
|
+
};
|
|
20
|
+
const toolUses = (parts) => parts.filter((p) => p?.type === "tool-call")
|
|
21
|
+
.map((p) => toolUse(p.toolCallId, p.toolName, p.input, !!p.providerExecuted)).slice(0, 128);
|
|
22
|
+
/** For `toolApproval:` on generateText, streamText and ToolLoopAgent. */
|
|
23
|
+
const toolApproval = async ({ toolCall, tools }) => {
|
|
24
|
+
if (tools?.[toolCall.toolName]?.[EXECUTOR] || executors.has(toolCall.toolName))
|
|
25
|
+
return "not-applicable";
|
|
26
|
+
const id = toolCall.toolCallId;
|
|
27
|
+
// the arguments string stays only for a call whose `execute` will take it: not one denied, nor a tool without one
|
|
28
|
+
const args = tools?.[toolCall.toolName]?.execute ? raw.get(id) : take(raw, id);
|
|
29
|
+
let d;
|
|
30
|
+
try {
|
|
31
|
+
d = await gate.decide(id, named.get(toolCall.toolName) ?? toolCall.toolName, args ?? toolCall.input);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
raw.delete(id);
|
|
35
|
+
return { type: "denied", reason: BLOCKED + `signer refused the call: ${failure(e)}` };
|
|
36
|
+
}
|
|
37
|
+
if (d.decision === "deny") {
|
|
38
|
+
raw.delete(id);
|
|
39
|
+
return { type: "denied", reason: d.unavailable ? BLOCKED + d.reason : refusal(d.rule_ids) };
|
|
40
|
+
}
|
|
41
|
+
keep(decided, id, d);
|
|
42
|
+
if (d.decision !== "ask")
|
|
43
|
+
return "not-applicable"; // execute consumes
|
|
44
|
+
try {
|
|
45
|
+
await gate.runOf(id).approvalRequest(id, { attempt: gate.attempt(id) });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return "not-applicable"; // no approval to wait for: execute's approval_consume refuses the call
|
|
49
|
+
}
|
|
50
|
+
return { type: "user-approval", reason: d.rule_ids.join(", ") };
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
middleware: {
|
|
54
|
+
specificationVersion: "v4",
|
|
55
|
+
async wrapGenerate({ doGenerate, model }) {
|
|
56
|
+
const r = await doGenerate();
|
|
57
|
+
(r.content ?? []).forEach(seen);
|
|
58
|
+
await recordResponse(gate.run, model.provider, model.modelId, () => ({ content_digest: digest(r.content ?? []),
|
|
59
|
+
stop_reason: stop(r.finishReason), usage: usage(r.usage), tool_uses: toolUses(r.content ?? []) }));
|
|
60
|
+
return r;
|
|
61
|
+
},
|
|
62
|
+
async wrapStream({ doStream, model }) {
|
|
63
|
+
const r = await doStream(), calls = [], at = gate.run; // the run current when the stream began
|
|
64
|
+
let finish;
|
|
65
|
+
return { ...r, stream: r.stream.pipeThrough(new TransformStream({
|
|
66
|
+
transform(p, c) {
|
|
67
|
+
seen(p);
|
|
68
|
+
if (p?.type === "tool-call")
|
|
69
|
+
calls.push(p);
|
|
70
|
+
if (p?.type === "finish")
|
|
71
|
+
finish = p;
|
|
72
|
+
c.enqueue(p);
|
|
73
|
+
},
|
|
74
|
+
// lean: a stream the caller abandons is not recorded; record it on cancel() if abandoned streams matter
|
|
75
|
+
flush: () => recordResponse(at, model.provider, model.modelId, () => ({ streamed: true,
|
|
76
|
+
stop_reason: stop(finish?.finishReason), usage: usage(finish?.usage), tool_uses: toolUses(calls) })),
|
|
77
|
+
})) };
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
toolApproval,
|
|
81
|
+
/** `toolApproval` that also asks the app's own (a function, or one per tool name): the signer's policy first; the
|
|
82
|
+
* app's answer applies only to a call the policy allows. A call the app denies is completed as refused. */
|
|
83
|
+
toolApprovalWith(app) {
|
|
84
|
+
return async (opts) => {
|
|
85
|
+
const mine = await toolApproval(opts);
|
|
86
|
+
if (mine !== "not-applicable")
|
|
87
|
+
return mine;
|
|
88
|
+
const { toolCall } = opts, id = toolCall.toolCallId;
|
|
89
|
+
const own = typeof app === "function" ? app : app?.[toolCall.toolName];
|
|
90
|
+
const theirs = typeof own === "function"
|
|
91
|
+
? await (typeof app === "function" ? own(opts) : own(toolCall.input, { toolCallId: id, messages: opts.messages }))
|
|
92
|
+
: own;
|
|
93
|
+
if (theirs === "denied" || theirs?.type === "denied") {
|
|
94
|
+
take(decided, id);
|
|
95
|
+
await gate.runOf(id).complete(id, "error", { attempt: gate.attempt(id), error: "refused by the app's toolApproval" });
|
|
96
|
+
}
|
|
97
|
+
return theirs ?? mine;
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
/** `tools` with each `execute` gated by the signer. With `mcp` (the server's name: `tools(await mcp.tools(),
|
|
101
|
+
* { mcp: "filesystem" })`) each is decided as `mcp:<server>/<tool>`. */
|
|
102
|
+
tools(tools, { mcp } = {}) {
|
|
103
|
+
const as = (name) => mcp === undefined ? name : `mcp:${mcp.replace(/[^A-Za-z0-9_.-]+/g, "_") || "unknown"}/${name}`;
|
|
104
|
+
if (mcp !== undefined)
|
|
105
|
+
for (const name of Object.keys(tools))
|
|
106
|
+
named.set(name, as(name));
|
|
107
|
+
return Object.fromEntries(Object.entries(tools).map(([name, t]) => [name, !t?.execute ? t
|
|
108
|
+
: t[EXECUTOR] ? (executors.add(name), t) : {
|
|
109
|
+
...t,
|
|
110
|
+
async execute(input, opts) {
|
|
111
|
+
const id = opts?.toolCallId;
|
|
112
|
+
const why = await gate.admit(id, as(name), take(raw, id) ?? input, { d: take(decided, id) });
|
|
113
|
+
if (why !== null)
|
|
114
|
+
throw new Error(why);
|
|
115
|
+
return gate.execute(id, () => t.execute(gate.input(id, input), opts));
|
|
116
|
+
},
|
|
117
|
+
}]));
|
|
118
|
+
},
|
|
119
|
+
/** `tool-approval-response` parts for the `tool-approval-request` parts the signer's approvers decided. */
|
|
120
|
+
async approvalResponses(parts) {
|
|
121
|
+
const states = await approvalStates(gate.run);
|
|
122
|
+
return parts.filter((p) => p?.type === "tool-approval-request").flatMap((p) => {
|
|
123
|
+
const s = states.get(p.toolCall?.toolCallId ?? p.toolCallId);
|
|
124
|
+
if (s === "approved")
|
|
125
|
+
return [{ type: "tool-approval-response", approvalId: p.approvalId, approved: true }];
|
|
126
|
+
if (s === "rejected" || s === "expired")
|
|
127
|
+
return [{ type: "tool-approval-response", approvalId: p.approvalId, approved: false, reason: `Tool call ${s} in Tracekit` }];
|
|
128
|
+
return [];
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { type Socket } from "node:net";
|
|
2
|
+
import { TracekitAgents } from "./adapters/openai-agents.js";
|
|
3
|
+
import { tracekitAI } from "./adapters/vercel-ai.js";
|
|
4
|
+
import { RPC_VERSION } from "./rpc_schema.js";
|
|
5
|
+
export { RPC_VERSION };
|
|
6
|
+
export { argsDigest, canonicalize, digest, strictParse, StrictJSONError } from "./jcs.js";
|
|
7
|
+
export declare const SYSTEM_CONFIG = "/etc/tracekit/client.json";
|
|
8
|
+
export type Frame = Record<string, any>;
|
|
9
|
+
export declare class SignerUnavailable extends Error {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
/** The signer answered but does not speak this client's protocol version. It is left running. */
|
|
13
|
+
export declare class Incompatible extends Error {
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
/** A refusal from the signer (or of a request that fails the contract before it is sent). */
|
|
17
|
+
export declare class RPCError extends Error {
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly retryAfterMs?: number | undefined;
|
|
20
|
+
name: string;
|
|
21
|
+
constructor(code: string, message: string, retryAfterMs?: number | undefined);
|
|
22
|
+
}
|
|
23
|
+
/** Newline-delimited JSON frames on one socket; answers are matched to requests in order. */
|
|
24
|
+
declare class Conn {
|
|
25
|
+
readonly sock: Socket;
|
|
26
|
+
private pending;
|
|
27
|
+
private buf;
|
|
28
|
+
constructor(sock: Socket);
|
|
29
|
+
write(frame: Frame): void;
|
|
30
|
+
send(frame: Frame): Promise<Frame>;
|
|
31
|
+
}
|
|
32
|
+
/** A connection to the signer at `path` (a Unix socket or tcp://host:port) and its hello, after the version check. */
|
|
33
|
+
export declare function connect(path: string, token?: string): Promise<{
|
|
34
|
+
conn: Conn;
|
|
35
|
+
hello: Frame;
|
|
36
|
+
}>;
|
|
37
|
+
/** The root-owned system-mode client config, or null when there is none; throws when it exists but can't be trusted. */
|
|
38
|
+
export declare function systemConfig(path?: string): Frame | null;
|
|
39
|
+
/** $TRACEKIT_SIGNER, or in system mode the system signer: there a $TRACEKIT_SIGNER that names another is refused. */
|
|
40
|
+
export declare function defaultSigner(env?: NodeJS.ProcessEnv, system?: Frame | null): string | null;
|
|
41
|
+
/** The dev signer's runtime dir, as tracekit/sdk/autospawn.py `runtime_dir` names it. */
|
|
42
|
+
export declare function runtimeDir(env?: NodeJS.ProcessEnv): string;
|
|
43
|
+
/** The runtime dir of the same-user dev signer; refused in system mode, as tracekit/sdk/autospawn.py `ensure` does. */
|
|
44
|
+
export declare function devRuntimeDir(system?: string): string;
|
|
45
|
+
/** The URL for the run or approval `frame` names, from the signer's `url`. A central signer's replica
|
|
46
|
+
* (docs/deploy-kubernetes.md) prefixes its ids with its route, `<service>-<n>.`: such an id goes to host `<service>-<n>`
|
|
47
|
+
* in the signer's domain, as the Python client does. */
|
|
48
|
+
export declare function routeUrl(url: URL, frame: Frame): URL;
|
|
49
|
+
export interface ClientOptions {
|
|
50
|
+
/** A Unix socket path, tcp://host:port or https://host:port; by default as described above. */
|
|
51
|
+
signer?: string;
|
|
52
|
+
/** Per-call timeout (default 30 s), plus a call's own `timeout_ms`. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
}
|
|
55
|
+
/** Methods take the request object of an RPC and return its response, or reject with RPCError (a refusal),
|
|
56
|
+
* SignerUnavailable or Incompatible. */
|
|
57
|
+
export declare class Client {
|
|
58
|
+
readonly signer: string | null;
|
|
59
|
+
readonly stream: string;
|
|
60
|
+
readonly timeoutMs: number;
|
|
61
|
+
hello: Frame | null;
|
|
62
|
+
private https;
|
|
63
|
+
private conn;
|
|
64
|
+
private seqs;
|
|
65
|
+
private order;
|
|
66
|
+
private inflight;
|
|
67
|
+
constructor(opts?: ClientOptions);
|
|
68
|
+
/** Register a run (`agent` is a name or {name, version}, `parent` the handle of the run starting it, such as a
|
|
69
|
+
* sub-agent's parent: the signer records the link); the handle holds its run token. */
|
|
70
|
+
registerRun(agent: string | {
|
|
71
|
+
name: string;
|
|
72
|
+
version?: string;
|
|
73
|
+
}, fields?: Frame, parent?: RunHandle): Promise<RunHandle>;
|
|
74
|
+
/** `request_id`, `stream` and `client_seq` are filled in when missing. */
|
|
75
|
+
call(method: string, req?: Frame): Promise<Frame>;
|
|
76
|
+
/** Resolves once every call made so far, and every promise given to waitUntil, has settled. */
|
|
77
|
+
flush(): Promise<void>;
|
|
78
|
+
/** Have flush() and close() wait for `p` too (for serverless hosts that freeze the process after a response). */
|
|
79
|
+
waitUntil<T>(p: Promise<T>): Promise<T>;
|
|
80
|
+
/** Waits for what is in flight, then closes the connection. */
|
|
81
|
+
close(): Promise<void>;
|
|
82
|
+
private send;
|
|
83
|
+
/** Waits up to `ms` for the run's event before this one to be answered, so a run's events reach the signer in
|
|
84
|
+
* client_seq order on every transport and retry. -> what ends this event's turn; SignerUnavailable after `ms`. */
|
|
85
|
+
private turn;
|
|
86
|
+
/** One request on a connection of its own, closed after the answer. */
|
|
87
|
+
private poll;
|
|
88
|
+
private connection;
|
|
89
|
+
private dial;
|
|
90
|
+
}
|
|
91
|
+
/** Whether a call of `toolClass` may run while the signer cannot be reached: its entry in register_run's
|
|
92
|
+
* `fail_modes`, else their `default`, else closed. A refusal from the signer is never a reason to fail open. */
|
|
93
|
+
export declare function failOpen(failModes: Record<string, string> | undefined, toolClass?: string): boolean;
|
|
94
|
+
export interface Decision extends Frame {
|
|
95
|
+
decision: "allow" | "deny" | "ask";
|
|
96
|
+
rule_ids: string[];
|
|
97
|
+
/** Set when the signer could not be reached: the decision is the run's fail mode, and nothing was recorded. */
|
|
98
|
+
unavailable?: true;
|
|
99
|
+
/** Set for a tool the policy lists as untrusted: `complete` should carry the result itself, not its digest, so the
|
|
100
|
+
* signer can index its values for provenance. */
|
|
101
|
+
send_result?: true;
|
|
102
|
+
}
|
|
103
|
+
/** A registered run. Its methods fill in run_id and run_token; `fields` adds optional request fields. */
|
|
104
|
+
export declare class RunHandle {
|
|
105
|
+
readonly client: Client;
|
|
106
|
+
runId: string;
|
|
107
|
+
runToken: string;
|
|
108
|
+
failModes: Record<string, string> | undefined;
|
|
109
|
+
registered: Frame;
|
|
110
|
+
closed: boolean;
|
|
111
|
+
/** The agent name instrument()'s process run renews itself as once the signer closed it after it was idle; null:
|
|
112
|
+
* never renewed (a run the app registered itself, or one it closed). */
|
|
113
|
+
renewAs: string | null;
|
|
114
|
+
private decided;
|
|
115
|
+
private at;
|
|
116
|
+
private renewing;
|
|
117
|
+
constructor(client: Client, registered: Frame);
|
|
118
|
+
/** With $TRACEKIT_EGRESS_PROXY set (an egress observer, docs/egress.md#attribution): HTTPS_PROXY and HTTP_PROXY,
|
|
119
|
+
* both cases, naming it with this run's id and token as credentials, for the commands the run starts
|
|
120
|
+
* (`spawn(cmd, args, { env: { ...process.env, ...run.proxyEnv } })`) or an HTTP client's proxy, so the observer
|
|
121
|
+
* attributes their connections to this run. {} without it. This process's own environment is left as it is. */
|
|
122
|
+
get proxyEnv(): Record<string, string>;
|
|
123
|
+
/** Any per-run RPC with this run's id and token. A call's complete and approvals go to the run it was decided in;
|
|
124
|
+
* new work refused because the signer closed a renewable run goes to a new run (renewAs). */
|
|
125
|
+
call(method: string, fields?: Frame): Promise<Frame>;
|
|
126
|
+
private renew;
|
|
127
|
+
/** `args` is the model's raw arguments string, or an already parsed value. Only when the signer can't be reached
|
|
128
|
+
* does the run's fail mode for `fields.tool_class_hint` decide; a refusal rejects. */
|
|
129
|
+
decide(toolCallId: string, tool: string, args: unknown, fields?: Frame): Promise<Decision>;
|
|
130
|
+
/** Never rejects: a failure to record a call that already ran is a warning. Defaults decision_id and args_digest to
|
|
131
|
+
* those of the last `decide` for this tool call. */
|
|
132
|
+
complete(toolCallId: string, status?: "ok" | "error", fields?: Frame): Promise<Frame | null>;
|
|
133
|
+
approvalRequest(toolCallId: string, fields?: Frame): Promise<Frame>;
|
|
134
|
+
/** Waits up to `timeoutMs` (signer cap 300 s) for a decision, on a connection of its own. */
|
|
135
|
+
approvalWait(approvalId: string, timeoutMs?: number): Promise<Frame>;
|
|
136
|
+
/** Records content that entered the run other than as a tool result (a mail, a ticket, a page the app fetched) for
|
|
137
|
+
* the signer's provenance index; the task or user prompt goes in with trust "trusted". Only its commitment is recorded. */
|
|
138
|
+
observe(value: unknown, source: string, trust?: "trusted" | "untrusted", fields?: Frame): Promise<Frame>;
|
|
139
|
+
stateWrite(key: string, valueDigest: string, fields?: Frame): Promise<Frame>;
|
|
140
|
+
modelEvent(provider: string, model: string, phase: "request" | "response", fields?: Frame): Promise<Frame>;
|
|
141
|
+
close(reason?: string): Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
/** Runs `fn` with `handle` as the current run of everything it awaits. */
|
|
144
|
+
export declare function withRun<T>(handle: RunHandle, fn: () => T): T;
|
|
145
|
+
export declare function currentRun(): RunHandle | undefined;
|
|
146
|
+
export interface Instrumented {
|
|
147
|
+
run: RunHandle;
|
|
148
|
+
/** Vercel AI SDK: `wrapLanguageModel({ model, middleware: ai.middleware })`, `tools: ai.tools({...})`,
|
|
149
|
+
* `toolApproval: ai.toolApproval` (src/v2/adapters/vercel-ai.ts). */
|
|
150
|
+
ai: ReturnType<typeof tracekitAI>;
|
|
151
|
+
/** OpenAI Agents SDK: `agents.tool(tool, {...})` for each tool (src/v2/adapters/openai-agents.ts). */
|
|
152
|
+
agents: TracekitAgents;
|
|
153
|
+
}
|
|
154
|
+
/** One call: registers a run (with the same-user dev signer, started if none answers, unless a signer is configured as
|
|
155
|
+
* described above), closes it when the process is about to exit, and returns it with the Vercel AI SDK and OpenAI
|
|
156
|
+
* Agents adapters bound to it. A second call returns the same. ES modules can't be patched from outside, so the
|
|
157
|
+
* adapters are handed to you to pass to the framework, rather than wired as `tracekit.instrument()` does in Python.
|
|
158
|
+
* `agent` names the run (default: $TRACEKIT_AGENT, else the script's name). */
|
|
159
|
+
export declare function instrument(agent?: string, opts?: ClientOptions): Promise<Instrumented>;
|