@fusionkit/protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/api.d.ts +106 -0
  2. package/dist/api.js +6 -0
  3. package/dist/chain.d.ts +14 -0
  4. package/dist/chain.js +49 -0
  5. package/dist/constants.d.ts +25 -0
  6. package/dist/constants.js +36 -0
  7. package/dist/contract.d.ts +6 -0
  8. package/dist/contract.js +32 -0
  9. package/dist/execution.d.ts +67 -0
  10. package/dist/execution.js +27 -0
  11. package/dist/generated/model-fusion-openapi.d.ts +44 -0
  12. package/dist/generated/model-fusion-openapi.js +23 -0
  13. package/dist/hash.d.ts +10 -0
  14. package/dist/hash.js +31 -0
  15. package/dist/index.d.ts +42 -0
  16. package/dist/index.js +30 -0
  17. package/dist/jcs.d.ts +14 -0
  18. package/dist/jcs.js +49 -0
  19. package/dist/keys.d.ts +14 -0
  20. package/dist/keys.js +36 -0
  21. package/dist/model-fusion.d.ts +167 -0
  22. package/dist/model-fusion.js +596 -0
  23. package/dist/receipt-story.d.ts +27 -0
  24. package/dist/receipt-story.js +127 -0
  25. package/dist/receipt.d.ts +20 -0
  26. package/dist/receipt.js +162 -0
  27. package/dist/test/model-fusion.test.d.ts +1 -0
  28. package/dist/test/model-fusion.test.js +213 -0
  29. package/dist/test/protocol.test.d.ts +1 -0
  30. package/dist/test/protocol.test.js +240 -0
  31. package/dist/test/tool-executor.test.d.ts +1 -0
  32. package/dist/test/tool-executor.test.js +86 -0
  33. package/dist/test/trace.test.d.ts +1 -0
  34. package/dist/test/trace.test.js +75 -0
  35. package/dist/tool-executor.d.ts +58 -0
  36. package/dist/tool-executor.js +80 -0
  37. package/dist/trace.d.ts +119 -0
  38. package/dist/trace.js +248 -0
  39. package/dist/types.d.ts +375 -0
  40. package/dist/types.js +14 -0
  41. package/dist/validators.d.ts +7 -0
  42. package/dist/validators.js +32 -0
  43. package/dist/vocabulary.d.ts +12 -0
  44. package/dist/vocabulary.js +79 -0
  45. package/package.json +27 -0
@@ -0,0 +1,240 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { appendEvent, verifyChain } from "../chain.js";
4
+ import { contractHash, signContract } from "../contract.js";
5
+ import { canonicalize } from "../jcs.js";
6
+ import { hashCanonical } from "../hash.js";
7
+ import { generateEd25519KeyPair, keyIdFromPublicPem, signData, verifyData } from "../keys.js";
8
+ import { signReceipt, verifyReceiptBundle, verifyRunnerReceipt } from "../receipt.js";
9
+ import { buildReceiptStory } from "../receipt-story.js";
10
+ import { AGENT_KINDS, isAgentKind, isTerminalStatus, parseSecretName, parseWorkspaceManifestPath, RUN_STATUSES, SESSION_ISOLATIONS } from "../index.js";
11
+ test("canonicalize sorts keys and is whitespace-free", () => {
12
+ const value = { b: 2, a: { d: [1, 2, { z: true, y: null }], c: "x" } };
13
+ assert.equal(canonicalize(value), '{"a":{"c":"x","d":[1,2,{"y":null,"z":true}]},"b":2}');
14
+ });
15
+ test("canonicalize uses ES number serialization", () => {
16
+ assert.equal(canonicalize({ n: 1e21 }), '{"n":1e+21}');
17
+ assert.equal(canonicalize({ n: 0.000001 }), '{"n":0.000001}');
18
+ assert.equal(canonicalize({ n: 10 }), '{"n":10}');
19
+ assert.throws(() => canonicalize({ n: Infinity }));
20
+ });
21
+ test("canonicalize is order-insensitive for equal objects", () => {
22
+ const a = { x: 1, y: [true, "s"] };
23
+ const b = { y: [true, "s"], x: 1 };
24
+ assert.equal(hashCanonical(a), hashCanonical(b));
25
+ });
26
+ test("ed25519 sign/verify roundtrip and tamper detection", () => {
27
+ const keys = generateEd25519KeyPair();
28
+ const payload = "warrant test payload";
29
+ const sig = signData(keys.privateKeyPem, payload);
30
+ assert.equal(verifyData(keys.publicKeyPem, payload, sig), true);
31
+ assert.equal(verifyData(keys.publicKeyPem, payload + "x", sig), false);
32
+ const other = generateEd25519KeyPair();
33
+ assert.equal(verifyData(other.publicKeyPem, payload, sig), false);
34
+ assert.match(keyIdFromPublicPem(keys.publicKeyPem), /^ed25519:[0-9a-f]{16}$/);
35
+ });
36
+ test("event chain appends and verifies; tampering breaks it", () => {
37
+ const genesis = hashCanonical({ contract: "fake" });
38
+ const chain = [];
39
+ appendEvent(chain, { type: "run.created" }, genesis);
40
+ appendEvent(chain, { type: "policy.evaluated", decision: "allow", reason: "test" }, genesis);
41
+ appendEvent(chain, { type: "run.completed" }, genesis);
42
+ assert.deepEqual(verifyChain(chain, genesis), { ok: true });
43
+ const tampered = structuredClone(chain);
44
+ const second = tampered[1];
45
+ assert.ok(second);
46
+ second.event = {
47
+ type: "policy.evaluated",
48
+ decision: "allow",
49
+ reason: "rewritten history"
50
+ };
51
+ const result = verifyChain(tampered, genesis);
52
+ assert.equal(result.ok, false);
53
+ if (!result.ok)
54
+ assert.equal(result.brokenAtSeq, 1);
55
+ const dropped = chain.slice(1);
56
+ const droppedResult = verifyChain(dropped, genesis);
57
+ assert.equal(droppedResult.ok, false);
58
+ });
59
+ test("protocol vocabulary is canonical and guarded", () => {
60
+ assert.deepEqual(AGENT_KINDS, ["claude-code", "codex", "pi", "mock", "command"]);
61
+ assert.deepEqual(SESSION_ISOLATIONS, [
62
+ "process",
63
+ "hermetic",
64
+ "vercel-sandbox"
65
+ ]);
66
+ assert.ok(RUN_STATUSES.includes("awaiting_approval"));
67
+ assert.equal(isAgentKind("command"), true);
68
+ assert.equal(isAgentKind("shell"), false);
69
+ assert.equal(isTerminalStatus("completed"), true);
70
+ assert.equal(isTerminalStatus("running"), false);
71
+ });
72
+ test("protocol validators reject unsafe workspace paths and secret names", () => {
73
+ assert.equal(parseWorkspaceManifestPath("src/index.ts"), "src/index.ts");
74
+ assert.throws(() => parseWorkspaceManifestPath("../escape"));
75
+ assert.throws(() => parseWorkspaceManifestPath("/absolute"));
76
+ assert.throws(() => parseWorkspaceManifestPath("a/../escape"));
77
+ assert.equal(parseSecretName("API_TOKEN"), "API_TOKEN");
78
+ assert.throws(() => parseSecretName("bad-name"));
79
+ });
80
+ function receiptFixture(isolation = "process") {
81
+ const plane = generateEd25519KeyPair();
82
+ const runner = generateEd25519KeyPair();
83
+ const workspace = {
84
+ version: "warrant.manifest.v1",
85
+ baseRef: "abc123",
86
+ bundleHash: "1".repeat(64),
87
+ untrackedFiles: [],
88
+ deniedPatterns: [],
89
+ deniedPaths: []
90
+ };
91
+ const contract = signContract({
92
+ version: "warrant.contract.v1",
93
+ runId: "run_test",
94
+ issuedAt: "2026-06-11T00:00:00.000Z",
95
+ issuer: { keyId: keyIdFromPublicPem(plane.publicKeyPem), role: "plane" },
96
+ requestedBy: { kind: "human", id: "alice" },
97
+ agent: { kind: "command" },
98
+ task: { prompt: "echo hi" },
99
+ runner: { pool: "default" },
100
+ workspace,
101
+ policyHash: "2".repeat(64),
102
+ secrets: [{ name: "API_TOKEN", scope: "pool:default" }],
103
+ network: { defaultDeny: true, allowHosts: [] },
104
+ budget: {},
105
+ disclosure: "minimal-context",
106
+ execution: { kind: "shell", script: "echo hi" },
107
+ expiresAt: "2026-06-11T01:00:00.000Z",
108
+ signatures: []
109
+ }, plane.privateKeyPem, plane.publicKeyPem, "plane");
110
+ const genesis = contractHash(contract);
111
+ const events = [];
112
+ appendEvent(events, { type: "run.created" }, genesis);
113
+ appendEvent(events, { type: "secret.released", name: "API_TOKEN", scope: "pool:default" }, genesis);
114
+ appendEvent(events, { type: "command.executed", argvHash: "3".repeat(64), exitCode: 0 }, genesis);
115
+ appendEvent(events, { type: "run.completed" }, genesis);
116
+ const secretEvent = events.find((event) => event.event.type === "secret.released");
117
+ assert.ok(secretEvent);
118
+ const last = events.at(-1);
119
+ assert.ok(last);
120
+ const receipt = signReceipt({
121
+ version: "warrant.receipt.v1",
122
+ runId: "run_test",
123
+ contractHash: genesis,
124
+ runner: {
125
+ runnerId: "rnr_test",
126
+ keyId: keyIdFromPublicPem(runner.publicKeyPem),
127
+ pool: "default",
128
+ attestationTier: "standard",
129
+ isolation
130
+ },
131
+ startedAt: "2026-06-11T00:00:00.000Z",
132
+ endedAt: "2026-06-11T00:00:01.000Z",
133
+ status: "completed",
134
+ eventsHead: last.hash,
135
+ eventCount: events.length,
136
+ workspaceIn: {
137
+ baseRef: workspace.baseRef,
138
+ manifestHash: hashCanonical(workspace)
139
+ },
140
+ workspaceOut: { diffHash: "", artifactHashes: [] },
141
+ secretsReleased: [
142
+ {
143
+ name: "API_TOKEN",
144
+ scope: "pool:default",
145
+ ts: secretEvent.ts
146
+ }
147
+ ],
148
+ networkAccessed: [],
149
+ modelsUsed: [],
150
+ boundaryDisclosures: [],
151
+ signatures: []
152
+ }, runner.privateKeyPem, runner.publicKeyPem, "runner");
153
+ return {
154
+ contract,
155
+ receipt,
156
+ events,
157
+ planePrivateKeyPem: plane.privateKeyPem,
158
+ planePublicKeyPem: plane.publicKeyPem,
159
+ runnerPublicKeyPem: runner.publicKeyPem
160
+ };
161
+ }
162
+ test("verifyRunnerReceipt checks pre-countersign receipt evidence", () => {
163
+ const fixture = receiptFixture();
164
+ assert.deepEqual(verifyRunnerReceipt({
165
+ contract: fixture.contract,
166
+ receipt: fixture.receipt,
167
+ events: fixture.events,
168
+ runnerPublicKeyPem: fixture.runnerPublicKeyPem
169
+ }), { ok: true, problems: [] });
170
+ const tampered = structuredClone(fixture.receipt);
171
+ tampered.eventCount += 1;
172
+ const result = verifyRunnerReceipt({
173
+ contract: fixture.contract,
174
+ receipt: tampered,
175
+ events: fixture.events,
176
+ runnerPublicKeyPem: fixture.runnerPublicKeyPem
177
+ });
178
+ assert.equal(result.ok, false);
179
+ assert.ok(result.problems.includes("receipt.eventCount does not match the event chain"));
180
+ const forgedScope = structuredClone(fixture.receipt);
181
+ forgedScope.secretsReleased[0] = {
182
+ ...forgedScope.secretsReleased[0],
183
+ scope: "pool:other"
184
+ };
185
+ const scopeResult = verifyRunnerReceipt({
186
+ contract: fixture.contract,
187
+ receipt: forgedScope,
188
+ events: fixture.events,
189
+ runnerPublicKeyPem: fixture.runnerPublicKeyPem
190
+ });
191
+ assert.equal(scopeResult.ok, false);
192
+ assert.ok(scopeResult.problems.includes("secretsReleased does not match secret.released events"));
193
+ const forgedTimestamp = structuredClone(fixture.receipt);
194
+ forgedTimestamp.secretsReleased[0] = {
195
+ ...forgedTimestamp.secretsReleased[0],
196
+ ts: "2026-06-11T00:00:00.999Z"
197
+ };
198
+ const timestampResult = verifyRunnerReceipt({
199
+ contract: fixture.contract,
200
+ receipt: forgedTimestamp,
201
+ events: fixture.events,
202
+ runnerPublicKeyPem: fixture.runnerPublicKeyPem
203
+ });
204
+ assert.equal(timestampResult.ok, false);
205
+ assert.ok(timestampResult.problems.includes("secretsReleased does not match secret.released events"));
206
+ });
207
+ test("receipt story is the canonical CLI/UI summary model", () => {
208
+ const fixture = receiptFixture();
209
+ const story = buildReceiptStory({
210
+ version: "warrant.bundle.v1",
211
+ contract: fixture.contract,
212
+ receipt: fixture.receipt,
213
+ events: fixture.events,
214
+ keys: {
215
+ planePublicKeyPem: generateEd25519KeyPair().publicKeyPem,
216
+ runnerPublicKeyPem: fixture.runnerPublicKeyPem
217
+ }
218
+ });
219
+ assert.equal(story.runId, "run_test");
220
+ assert.equal(story.status, "completed");
221
+ assert.equal(story.agent, "command");
222
+ assert.deepEqual(story.secrets, ["API_TOKEN (pool:default)"]);
223
+ });
224
+ test("vercel-sandbox receipts verify offline and render through receipt story", () => {
225
+ const fixture = receiptFixture("vercel-sandbox");
226
+ const receipt = signReceipt(fixture.receipt, fixture.planePrivateKeyPem, fixture.planePublicKeyPem, "plane");
227
+ const bundle = {
228
+ version: "warrant.bundle.v1",
229
+ contract: fixture.contract,
230
+ receipt,
231
+ events: fixture.events,
232
+ keys: {
233
+ planePublicKeyPem: fixture.planePublicKeyPem,
234
+ runnerPublicKeyPem: fixture.runnerPublicKeyPem
235
+ }
236
+ };
237
+ assert.deepEqual(verifyReceiptBundle(bundle), { ok: true, problems: [] });
238
+ const story = buildReceiptStory(bundle);
239
+ assert.equal(story.isolation, "vercel-sandbox");
240
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,86 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { evaluateToolPolicy, modelFusionSideEffects, toolArgumentsHash, toolCallKey, toolSideEffectClassFromModelFusion } from "../tool-executor.js";
4
+ import { MODEL_FUSION_SCHEMA_BUNDLE_HASH } from "../model-fusion.js";
5
+ const contract = {
6
+ executor_id: "exec_demo",
7
+ mode: "demo_safe",
8
+ environment_id: "env_local",
9
+ tool_policy_id: "policy_readonly",
10
+ allowed_tools: ["read_file", "echo"],
11
+ side_effects: ["none", "read"],
12
+ limits: { timeoutMs: 1000, maxOutputBytes: 1024 },
13
+ timeoutMs: 1000,
14
+ budget: { maxSpendUsd: 0 },
15
+ audit_sink: "memory"
16
+ };
17
+ test("tool call keys are stable and policy scoped", () => {
18
+ const request = {
19
+ tool_name: "read_file",
20
+ arguments: { path: "README.md" },
21
+ side_effects: "read"
22
+ };
23
+ assert.equal(toolArgumentsHash(request.arguments).startsWith("sha256:"), true);
24
+ assert.equal(toolCallKey({ contract, request }), toolCallKey({ contract, request }));
25
+ assert.notEqual(toolCallKey({ contract, request }), toolCallKey({
26
+ contract: { ...contract, environment_id: "env_other" },
27
+ request
28
+ }));
29
+ assert.equal(toolCallKey({ contract, request }), toolCallKey({
30
+ contract,
31
+ request: {
32
+ ...request,
33
+ candidate_id: "candidate_b",
34
+ plan_id: "tool_plan_other"
35
+ }
36
+ }));
37
+ });
38
+ test("tool policy allows read-only configured tools and denies unsafe calls", () => {
39
+ const allowed = evaluateToolPolicy(contract, {
40
+ tool_name: "read_file",
41
+ arguments: { path: "README.md" },
42
+ side_effects: "read"
43
+ });
44
+ assert.equal(allowed.decision, "allow");
45
+ assert.ok(allowed.decision === "allow" && allowed.dedupeKey);
46
+ const unknown = evaluateToolPolicy(contract, {
47
+ tool_name: "write_file",
48
+ arguments: { path: "README.md" },
49
+ side_effects: "write"
50
+ });
51
+ assert.equal(unknown.decision, "deny");
52
+ const external = evaluateToolPolicy({ ...contract, allowed_tools: ["fetch"], side_effects: ["external"] }, { tool_name: "fetch", arguments: { url: "https://example.com" }, side_effects: "external" });
53
+ assert.equal(external.decision, "deny");
54
+ });
55
+ test("tool side effects map to model-fusion side effects", () => {
56
+ assert.equal(modelFusionSideEffects("none"), "none");
57
+ assert.equal(modelFusionSideEffects("read"), "read_only");
58
+ assert.equal(modelFusionSideEffects("write"), "writes_workspace");
59
+ assert.equal(modelFusionSideEffects("external"), "network");
60
+ assert.equal(toolSideEffectClassFromModelFusion("none"), "none");
61
+ assert.equal(toolSideEffectClassFromModelFusion("read_only"), "read");
62
+ assert.equal(toolSideEffectClassFromModelFusion("writes_workspace"), "write");
63
+ assert.equal(toolSideEffectClassFromModelFusion("network"), "external");
64
+ assert.throws(() => toolSideEffectClassFromModelFusion("unknown"));
65
+ });
66
+ test("tool execution result shape remains JSON-safe", () => {
67
+ const result = {
68
+ record: {
69
+ schema: "tool-execution-record.v1",
70
+ schema_version: "v1",
71
+ schema_bundle_hash: MODEL_FUSION_SCHEMA_BUNDLE_HASH,
72
+ producer: "test",
73
+ producer_version: "0.1.0",
74
+ producer_git_sha: "0".repeat(40),
75
+ created_at: "2026-06-16T00:00:00.000Z",
76
+ execution_id: "exec_1",
77
+ plan_id: "plan_1",
78
+ status: "succeeded",
79
+ output_hash: toolArgumentsHash({ ok: true })
80
+ },
81
+ output: { ok: true },
82
+ deduped: false,
83
+ decision: { decision: "allow", reason: "test" }
84
+ };
85
+ assert.doesNotThrow(() => JSON.stringify(result));
86
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { assertFusionTraceEvent, FUSION_TRACE_EVENT_SCHEMA, isFusionTraceEvent, judgeFinalPayload, judgeRequestPayload, judgeThinkingPayload, modelCallFinishedPayload, modelCallStartedPayload } from "../trace.js";
4
+ function validEvent(overrides = {}) {
5
+ return {
6
+ schema: FUSION_TRACE_EVENT_SCHEMA,
7
+ trace_id: "trace_x",
8
+ span_id: "span_x",
9
+ seq: 0,
10
+ ts: Date.now(),
11
+ component: "judge",
12
+ event_type: "judge.request",
13
+ ...overrides
14
+ };
15
+ }
16
+ test("assertFusionTraceEvent accepts a well-formed event", () => {
17
+ assert.doesNotThrow(() => assertFusionTraceEvent(validEvent()));
18
+ assert.equal(isFusionTraceEvent(validEvent()), true);
19
+ });
20
+ test("assertFusionTraceEvent rejects malformed events", () => {
21
+ assert.throws(() => assertFusionTraceEvent(null));
22
+ assert.throws(() => assertFusionTraceEvent(validEvent({ schema: "nope" })));
23
+ assert.throws(() => assertFusionTraceEvent(validEvent({ component: "martian" })));
24
+ assert.throws(() => assertFusionTraceEvent(validEvent({ event_type: "nope" })));
25
+ assert.throws(() => assertFusionTraceEvent(validEvent({ trace_id: "" })));
26
+ assert.throws(() => assertFusionTraceEvent(validEvent({ seq: "x" })));
27
+ assert.throws(() => assertFusionTraceEvent(validEvent({ payload: [] })));
28
+ assert.equal(isFusionTraceEvent(validEvent({ component: "x" })), false);
29
+ });
30
+ test("judgeRequestPayload carries the full prompt fields and the turn", () => {
31
+ const payload = judgeRequestPayload({
32
+ judgeModel: "j",
33
+ messages: [{ role: "user" }],
34
+ trajectories: [{ trajectory_id: "t" }],
35
+ tools: [],
36
+ trajectoryIds: ["t"],
37
+ turn: 2
38
+ });
39
+ assert.equal(payload.judge_model, "j");
40
+ assert.deepEqual(payload.trajectory_ids, ["t"]);
41
+ assert.equal(payload.turn, 2);
42
+ assert.ok(Array.isArray(payload.messages));
43
+ assert.ok(Array.isArray(payload.trajectories));
44
+ });
45
+ test("judgeThinkingPayload carries interim analysis, tool calls, and turn", () => {
46
+ const payload = judgeThinkingPayload({
47
+ rawAnalysis: "requested a tool",
48
+ toolCalls: [{ id: "t1" }],
49
+ turn: 3
50
+ });
51
+ assert.equal(payload.raw_analysis, "requested a tool");
52
+ assert.deepEqual(payload.tool_calls, [{ id: "t1" }]);
53
+ assert.equal(payload.turn, 3);
54
+ });
55
+ test("judgeFinalPayload mirrors the final output into the record", () => {
56
+ const payload = judgeFinalPayload({ content: "answer", usage: { total_tokens: 5 } });
57
+ assert.equal(payload.final_output, "answer");
58
+ assert.deepEqual(payload.record, { final_output: "answer" });
59
+ assert.deepEqual(payload.usage, { total_tokens: 5 });
60
+ });
61
+ test("model-call payloads use snake_case prompt + token fields", () => {
62
+ const started = modelCallStartedPayload({ model: "m", systemPrompt: "sys", prompt: "task", tools: ["run"] });
63
+ assert.equal(started.system_prompt, "sys");
64
+ assert.equal(started.prompt, "task");
65
+ assert.deepEqual(started.tools, ["run"]);
66
+ const finished = modelCallFinishedPayload({
67
+ model: "m",
68
+ finalOutput: "done",
69
+ finishReason: "stop",
70
+ usage: { total_tokens: 9 }
71
+ });
72
+ assert.equal(finished.final_output, "done");
73
+ assert.equal(finished.finish_reason, "stop");
74
+ assert.equal(finished.content_preview, "done");
75
+ });
@@ -0,0 +1,58 @@
1
+ import type { JsonValue } from "./jcs.js";
2
+ import type { ModelFusionSideEffects, ToolExecutionRecordV1 } from "./model-fusion.js";
3
+ export type ToolSideEffectClass = "none" | "read" | "write" | "external";
4
+ export type ToolExecutorMode = "demo_safe" | "policy_bound";
5
+ export type ToolPolicyDecision = {
6
+ decision: "allow";
7
+ reason: string;
8
+ dedupeKey?: string;
9
+ } | {
10
+ decision: "deny";
11
+ reason: string;
12
+ errorKind: "tool_denied" | "capability_missing";
13
+ };
14
+ export type ToolDefinition = {
15
+ tool_name: string;
16
+ side_effects: ToolSideEffectClass;
17
+ description?: string;
18
+ };
19
+ export type ToolExecutorLimits = {
20
+ timeoutMs?: number;
21
+ maxOutputBytes?: number;
22
+ };
23
+ export type ToolExecutorBudget = {
24
+ maxSpendUsd?: number;
25
+ };
26
+ export type ToolExecutorContract = {
27
+ executor_id: string;
28
+ mode: ToolExecutorMode;
29
+ environment_id: string;
30
+ tool_policy_id: string;
31
+ allowed_tools: string[];
32
+ side_effects: ToolSideEffectClass[];
33
+ limits?: ToolExecutorLimits;
34
+ timeoutMs?: number;
35
+ budget?: ToolExecutorBudget;
36
+ audit_sink?: string;
37
+ };
38
+ export type ToolExecutionRequest = {
39
+ candidate_id?: string;
40
+ plan_id?: string;
41
+ tool_name: string;
42
+ arguments: JsonValue;
43
+ side_effects: ToolSideEffectClass;
44
+ };
45
+ export type ToolExecutionResult = {
46
+ record: ToolExecutionRecordV1;
47
+ output?: JsonValue;
48
+ deduped: boolean;
49
+ decision: ToolPolicyDecision;
50
+ };
51
+ export declare function toolArgumentsHash(args: JsonValue): string;
52
+ export declare function toolCallKey(input: {
53
+ contract: ToolExecutorContract;
54
+ request: ToolExecutionRequest;
55
+ }): string;
56
+ export declare function modelFusionSideEffects(sideEffects: ToolSideEffectClass): ModelFusionSideEffects;
57
+ export declare function toolSideEffectClassFromModelFusion(sideEffects: ModelFusionSideEffects): ToolSideEffectClass;
58
+ export declare function evaluateToolPolicy(contract: ToolExecutorContract, request: ToolExecutionRequest): ToolPolicyDecision;
@@ -0,0 +1,80 @@
1
+ import { hashCanonical, hashCanonicalSha256 } from "./hash.js";
2
+ export function toolArgumentsHash(args) {
3
+ return hashCanonicalSha256(args);
4
+ }
5
+ export function toolCallKey(input) {
6
+ return hashCanonical({
7
+ executor_id: input.contract.executor_id,
8
+ environment_id: input.contract.environment_id,
9
+ tool_policy_id: input.contract.tool_policy_id,
10
+ tool_name: input.request.tool_name,
11
+ side_effects: input.request.side_effects,
12
+ arguments_hash: toolArgumentsHash(input.request.arguments)
13
+ });
14
+ }
15
+ export function modelFusionSideEffects(sideEffects) {
16
+ switch (sideEffects) {
17
+ case "none":
18
+ return "none";
19
+ case "read":
20
+ return "read_only";
21
+ case "write":
22
+ return "writes_workspace";
23
+ case "external":
24
+ return "network";
25
+ default: {
26
+ const exhausted = sideEffects;
27
+ throw new Error(`unknown side effect class: ${String(exhausted)}`);
28
+ }
29
+ }
30
+ }
31
+ export function toolSideEffectClassFromModelFusion(sideEffects) {
32
+ switch (sideEffects) {
33
+ case "none":
34
+ return "none";
35
+ case "read_only":
36
+ return "read";
37
+ case "writes_workspace":
38
+ return "write";
39
+ case "network":
40
+ return "external";
41
+ case "tool_execution":
42
+ case "unknown":
43
+ throw new Error(`unsupported tool side effect: ${sideEffects}`);
44
+ default: {
45
+ const exhausted = sideEffects;
46
+ throw new Error(`unknown model-fusion side effect: ${String(exhausted)}`);
47
+ }
48
+ }
49
+ }
50
+ export function evaluateToolPolicy(contract, request) {
51
+ if (!contract.allowed_tools.includes(request.tool_name)) {
52
+ return {
53
+ decision: "deny",
54
+ reason: `tool ${request.tool_name} is not allowed by ${contract.tool_policy_id}`,
55
+ errorKind: "tool_denied"
56
+ };
57
+ }
58
+ if (!contract.side_effects.includes(request.side_effects)) {
59
+ return {
60
+ decision: "deny",
61
+ reason: `side effect ${request.side_effects} is not allowed by ${contract.tool_policy_id}`,
62
+ errorKind: "tool_denied"
63
+ };
64
+ }
65
+ if (contract.mode === "demo_safe" &&
66
+ (request.side_effects === "write" || request.side_effects === "external")) {
67
+ return {
68
+ decision: "deny",
69
+ reason: `demo_safe executor denies ${request.side_effects} tool calls by default`,
70
+ errorKind: "tool_denied"
71
+ };
72
+ }
73
+ return {
74
+ decision: "allow",
75
+ reason: "allowed by tool executor policy",
76
+ dedupeKey: request.side_effects === "none" || request.side_effects === "read"
77
+ ? toolCallKey({ contract, request })
78
+ : undefined
79
+ };
80
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * fusion-trace-event.v1 — fire-and-forget observability emitter.
3
+ *
4
+ * This is the canonical TypeScript implementation of the standalone fusion-trace
5
+ * contract (see fusionkit `spec/fusion-trace`). It lives in `@fusionkit/protocol`
6
+ * (a dependency-free leaf) so the gateway, ensemble harness, the AI SDK worktree
7
+ * agent, and the CLI can all emit against the same shape without import cycles.
8
+ *
9
+ * Emission is a no-op unless `FUSION_TRACE_URL` or `FUSION_TRACE_DIR` is set, so
10
+ * normal runs are never blocked by, or coupled to, the collector.
11
+ */
12
+ export declare const FUSION_TRACE_EVENT_SCHEMA: "fusion-trace-event.v1";
13
+ export declare const FUSION_TRACE_EVENT_VERSION: "1.0.0";
14
+ export declare const TRACE_ID_HEADER = "x-fusion-trace-id";
15
+ export declare const TRACE_SPAN_HEADER = "x-fusion-span-id";
16
+ export declare const TRACE_PARENT_SPAN_HEADER = "x-fusion-parent-span-id";
17
+ export declare const TRACE_CANDIDATE_HEADER = "x-fusion-candidate-id";
18
+ export type FusionTraceComponent = "gateway" | "ensemble" | "agent" | "panel-model" | "judge" | "synthesis" | "cursor-bridge";
19
+ export type FusionTraceEventType = "session.started" | "session.finished" | "harness.candidate.started" | "harness.candidate.finished" | "trajectory.step" | "model.call.started" | "model.call.finished" | "judge.request" | "judge.thinking" | "judge.scored" | "judge.synthesis" | "judge.final" | "tool.execution" | "cursor.route" | "log";
20
+ /** Runtime-iterable mirrors of the closed unions (used by the validator). */
21
+ export declare const FUSION_TRACE_COMPONENTS: readonly FusionTraceComponent[];
22
+ export declare const FUSION_TRACE_EVENT_TYPES: readonly FusionTraceEventType[];
23
+ export type FusionTraceEvent = {
24
+ schema: typeof FUSION_TRACE_EVENT_SCHEMA;
25
+ schema_version?: string;
26
+ trace_id: string;
27
+ span_id: string;
28
+ parent_span_id?: string;
29
+ seq: number;
30
+ ts: number;
31
+ component: FusionTraceComponent;
32
+ event_type: FusionTraceEventType;
33
+ session_id?: string;
34
+ candidate_id?: string;
35
+ model_id?: string;
36
+ payload?: Record<string, unknown>;
37
+ };
38
+ export type EmitInput = {
39
+ component: FusionTraceComponent;
40
+ event_type: FusionTraceEventType;
41
+ traceId?: string;
42
+ spanId?: string;
43
+ parentSpanId?: string;
44
+ candidateId?: string;
45
+ modelId?: string;
46
+ sessionId?: string;
47
+ payload?: Record<string, unknown>;
48
+ };
49
+ export declare function newTraceId(): string;
50
+ export declare function newSpanId(): string;
51
+ export declare function ambientTraceId(): string | undefined;
52
+ export declare class TraceEmitter {
53
+ private readonly url?;
54
+ private readonly dir?;
55
+ private readonly enabled;
56
+ private seq;
57
+ private dirReady;
58
+ constructor(config?: {
59
+ url?: string;
60
+ dir?: string;
61
+ });
62
+ isEnabled(): boolean;
63
+ emit(input: EmitInput): void;
64
+ private writeJsonl;
65
+ private post;
66
+ }
67
+ export declare function getTraceEmitter(): TraceEmitter;
68
+ export declare function emitTrace(input: EmitInput): void;
69
+ /**
70
+ * Assert that `value` is a well-formed wire `FusionTraceEvent`. Hand-written
71
+ * (Node-only, no deps) to match the `assertModelFusionRecord` style, so both the
72
+ * emitter side and the scope ingest boundary validate against one contract.
73
+ */
74
+ export declare function assertFusionTraceEvent(value: unknown): asserts value is FusionTraceEvent;
75
+ export declare function isFusionTraceEvent(value: unknown): value is FusionTraceEvent;
76
+ export declare function judgeRequestPayload(input: {
77
+ judgeModel?: string;
78
+ messages: unknown;
79
+ trajectories: unknown;
80
+ tools?: unknown;
81
+ toolChoice?: unknown;
82
+ trajectoryIds?: string[];
83
+ /** 1-based user-turn index (a follow-up message is a new turn). */
84
+ turn?: number;
85
+ }): Record<string, unknown>;
86
+ /** An intermediate (tool-calling) judge step within a turn. */
87
+ export declare function judgeThinkingPayload(input: {
88
+ rawAnalysis?: string;
89
+ toolCalls?: unknown;
90
+ usage?: unknown;
91
+ turn?: number;
92
+ }): Record<string, unknown>;
93
+ export declare function judgeFinalPayload(input: {
94
+ finalOutput?: string;
95
+ content?: string;
96
+ toolCalls?: unknown;
97
+ usage?: unknown;
98
+ httpStatus?: number;
99
+ error?: string;
100
+ turn?: number;
101
+ }): Record<string, unknown>;
102
+ export declare function modelCallStartedPayload(input: {
103
+ model: string;
104
+ systemPrompt?: string;
105
+ prompt?: string;
106
+ tools?: string[];
107
+ turn?: number;
108
+ }): Record<string, unknown>;
109
+ export declare function modelCallFinishedPayload(input: {
110
+ model: string;
111
+ finalOutput?: string;
112
+ usage?: unknown;
113
+ finishReason?: string;
114
+ stepCount?: number;
115
+ toolCallCount?: number;
116
+ latencyS?: number;
117
+ error?: string;
118
+ turn?: number;
119
+ }): Record<string, unknown>;