@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,127 @@
1
+ function short(hash, length = 12) {
2
+ return hash.slice(0, length);
3
+ }
4
+ export function summarizeRunEvent(event) {
5
+ switch (event.type) {
6
+ case "run.created":
7
+ return { tone: "info", label: "run.created", detail: "contract issued" };
8
+ case "run.claimed":
9
+ return {
10
+ tone: "info",
11
+ label: "run.claimed",
12
+ detail: `${event.runnerId} ${event.runnerKeyId}`
13
+ };
14
+ case "workspace.materialized":
15
+ return {
16
+ tone: "info",
17
+ label: "workspace.materialized",
18
+ detail: `manifest ${short(event.manifestHash)}`
19
+ };
20
+ case "policy.evaluated":
21
+ return {
22
+ tone: event.decision === "allow" ? "ok" : "warn",
23
+ label: "policy.evaluated",
24
+ detail: event.reason
25
+ };
26
+ case "consent.requested":
27
+ return {
28
+ tone: "warn",
29
+ label: "consent.requested",
30
+ detail: event.requirement
31
+ };
32
+ case "consent.granted":
33
+ return {
34
+ tone: "ok",
35
+ label: "consent.granted",
36
+ detail: `${event.actor.kind}:${event.actor.id}`
37
+ };
38
+ case "secret.released":
39
+ return {
40
+ tone: "warn",
41
+ label: "secret.released",
42
+ detail: `${event.name} (${event.scope})`
43
+ };
44
+ case "command.executed":
45
+ return {
46
+ tone: event.exitCode === 0 ? "ok" : "err",
47
+ label: "command.executed",
48
+ detail: `argv ${short(event.argvHash)} exit ${event.exitCode}`
49
+ };
50
+ case "file.changed":
51
+ return {
52
+ tone: "plain",
53
+ label: "file.changed",
54
+ detail: `${event.path} ${short(event.contentHash)}`
55
+ };
56
+ case "network.connected":
57
+ return {
58
+ tone: event.decision === "allowed" ? "ok" : "warn",
59
+ label: "network.connected",
60
+ detail: `${event.host} ${event.decision}`
61
+ };
62
+ case "model.called":
63
+ return {
64
+ tone: "info",
65
+ label: "model.called",
66
+ detail: `${event.provider}/${event.model}`
67
+ };
68
+ case "boundary.crossed":
69
+ return {
70
+ tone: "warn",
71
+ label: "boundary.crossed",
72
+ detail: `${event.direction}: ${event.dataClass} ${short(event.contentHash)}`
73
+ };
74
+ case "artifact.created":
75
+ return {
76
+ tone: "info",
77
+ label: "artifact.created",
78
+ detail: `${event.kind} ${short(event.hash)}`
79
+ };
80
+ case "checkpoint.created":
81
+ return {
82
+ tone: "info",
83
+ label: "checkpoint.created",
84
+ detail: `${event.checkpointId} (${event.tier})`
85
+ };
86
+ case "run.completed":
87
+ return { tone: "ok", label: "run.completed", detail: "completed" };
88
+ case "run.failed":
89
+ return {
90
+ tone: "err",
91
+ label: "run.failed",
92
+ detail: `${event.failure}: ${event.message}`
93
+ };
94
+ case "run.cancelled":
95
+ return {
96
+ tone: "warn",
97
+ label: "run.cancelled",
98
+ detail: `${event.actor.kind}:${event.actor.id}`
99
+ };
100
+ default: {
101
+ const exhausted = event;
102
+ return { tone: "plain", label: "unknown", detail: String(exhausted) };
103
+ }
104
+ }
105
+ }
106
+ export function buildReceiptStory(bundle) {
107
+ const { contract, receipt } = bundle;
108
+ return {
109
+ runId: receipt.runId,
110
+ status: receipt.status,
111
+ actor: `${contract.requestedBy.kind}:${contract.requestedBy.id}`,
112
+ agent: contract.agent.kind,
113
+ isolation: receipt.runner.isolation ?? "process",
114
+ workspace: {
115
+ baseRef: contract.workspace.baseRef,
116
+ manifestHash: receipt.workspaceIn.manifestHash,
117
+ diffHash: receipt.workspaceOut.diffHash,
118
+ artifactCount: receipt.workspaceOut.artifactHashes.length
119
+ },
120
+ policyHash: contract.policyHash,
121
+ secrets: receipt.secretsReleased.map((s) => `${s.name} (${s.scope})`),
122
+ network: receipt.networkAccessed.map((n) => `${n.host} ${n.decision}`),
123
+ eventCount: receipt.eventCount,
124
+ eventsHead: receipt.eventsHead,
125
+ verificationCommand: "warrant verify <bundle.json>"
126
+ };
127
+ }
@@ -0,0 +1,20 @@
1
+ import type { ChainedEvent, Receipt, ReceiptBundle, Signature } from "./types.js";
2
+ export declare function signReceipt(receipt: Receipt, privateKeyPem: string, publicKeyPem: string, signer: Signature["signer"]): Receipt;
3
+ export declare function verifyReceiptSignature(receipt: Receipt, signer: Signature["signer"], publicKeyPem: string): boolean;
4
+ export type BundleVerification = {
5
+ ok: boolean;
6
+ problems: string[];
7
+ };
8
+ export type RunnerReceiptVerificationInput = {
9
+ contract: ReceiptBundle["contract"];
10
+ receipt: Receipt;
11
+ events: ChainedEvent[];
12
+ runnerPublicKeyPem: string;
13
+ };
14
+ /**
15
+ * Verify the runner-produced receipt against the signed contract and stored
16
+ * event chain before any plane countersignature exists. This is the plane's
17
+ * completion-time check and the core of offline bundle verification.
18
+ */
19
+ export declare function verifyRunnerReceipt(input: RunnerReceiptVerificationInput): BundleVerification;
20
+ export declare function verifyReceiptBundle(bundle: ReceiptBundle): BundleVerification;
@@ -0,0 +1,162 @@
1
+ import { contractHash, verifyContractSignature } from "./contract.js";
2
+ import { hashCanonical } from "./hash.js";
3
+ import { keyIdFromPublicPem, signData, verifyData } from "./keys.js";
4
+ import { verifyChain } from "./chain.js";
5
+ function signablePayload(receipt) {
6
+ const { signatures: _signatures, ...unsigned } = receipt;
7
+ return hashCanonical(unsigned);
8
+ }
9
+ export function signReceipt(receipt, privateKeyPem, publicKeyPem, signer) {
10
+ const payload = signablePayload(receipt);
11
+ const signature = {
12
+ keyId: keyIdFromPublicPem(publicKeyPem),
13
+ alg: "ed25519",
14
+ signer,
15
+ sig: signData(privateKeyPem, payload)
16
+ };
17
+ return { ...receipt, signatures: [...receipt.signatures, signature] };
18
+ }
19
+ export function verifyReceiptSignature(receipt, signer, publicKeyPem) {
20
+ const payload = signablePayload(receipt);
21
+ const signature = receipt.signatures.find((s) => s.signer === signer);
22
+ if (!signature)
23
+ return false;
24
+ if (keyIdFromPublicPem(publicKeyPem) !== signature.keyId)
25
+ return false;
26
+ return verifyData(publicKeyPem, payload, signature.sig);
27
+ }
28
+ /** Element-wise equality for two already-sorted arrays. */
29
+ function arraysEqual(a, b) {
30
+ if (a.length !== b.length)
31
+ return false;
32
+ for (let i = 0; i < a.length; i++) {
33
+ if (a[i] !== b[i])
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ function secretReleaseKey(record) {
39
+ return `${record.name}\u0000${record.scope}\u0000${record.ts}`;
40
+ }
41
+ function terminalEventMatches(event, status) {
42
+ switch (status) {
43
+ case "completed":
44
+ return event.type === "run.completed";
45
+ case "failed":
46
+ return event.type === "run.failed";
47
+ case "cancelled":
48
+ return event.type === "run.cancelled";
49
+ default: {
50
+ const exhausted = status;
51
+ throw new Error(`unreachable status: ${String(exhausted)}`);
52
+ }
53
+ }
54
+ }
55
+ /**
56
+ * Verify the runner-produced receipt against the signed contract and stored
57
+ * event chain before any plane countersignature exists. This is the plane's
58
+ * completion-time check and the core of offline bundle verification.
59
+ */
60
+ export function verifyRunnerReceipt(input) {
61
+ const problems = [];
62
+ const { contract, receipt, events, runnerPublicKeyPem } = input;
63
+ const expectedContractHash = contractHash(contract);
64
+ if (receipt.runId !== contract.runId) {
65
+ problems.push("receipt.runId does not match the contract");
66
+ }
67
+ if (receipt.contractHash !== expectedContractHash) {
68
+ problems.push("receipt.contractHash does not match the contract");
69
+ }
70
+ if (receipt.runner.keyId !== keyIdFromPublicPem(runnerPublicKeyPem)) {
71
+ problems.push("receipt runner key id does not match the enrolled runner key");
72
+ }
73
+ if (receipt.runner.pool !== contract.runner.pool) {
74
+ problems.push("receipt runner pool does not match the contract");
75
+ }
76
+ if (!verifyReceiptSignature(receipt, "runner", runnerPublicKeyPem)) {
77
+ problems.push("receipt runner signature invalid");
78
+ }
79
+ const chain = verifyChain(events, expectedContractHash);
80
+ if (!chain.ok) {
81
+ problems.push(`event chain broken at seq ${chain.brokenAtSeq}: ${chain.reason}`);
82
+ }
83
+ const last = events[events.length - 1];
84
+ if (!last) {
85
+ problems.push("event chain is empty");
86
+ }
87
+ else {
88
+ if (receipt.eventsHead !== last.hash) {
89
+ problems.push("receipt.eventsHead does not match the last event hash");
90
+ }
91
+ if (receipt.eventCount !== events.length) {
92
+ problems.push("receipt.eventCount does not match the event chain");
93
+ }
94
+ if (!terminalEventMatches(last.event, receipt.status)) {
95
+ problems.push("terminal event does not match receipt status");
96
+ }
97
+ }
98
+ if (receipt.workspaceIn.baseRef !== contract.workspace.baseRef) {
99
+ problems.push("receipt.workspaceIn.baseRef does not match the contract");
100
+ }
101
+ const manifestHash = hashCanonical(contract.workspace);
102
+ if (receipt.workspaceIn.manifestHash !== manifestHash) {
103
+ problems.push("receipt.workspaceIn.manifestHash does not match the contract");
104
+ }
105
+ const releasedInEvents = events
106
+ .filter((e) => e.event.type === "secret.released")
107
+ .map((e) => e.event.type === "secret.released"
108
+ ? { name: e.event.name, scope: e.event.scope, ts: e.ts }
109
+ : { name: "", scope: "", ts: "" })
110
+ .map(secretReleaseKey)
111
+ .sort();
112
+ const releasedInReceipt = receipt.secretsReleased.map(secretReleaseKey).sort();
113
+ if (!arraysEqual(releasedInEvents, releasedInReceipt)) {
114
+ problems.push("secretsReleased does not match secret.released events");
115
+ }
116
+ if (receipt.workspaceOut.diffHash !== "") {
117
+ const artifactHashes = new Set(events
118
+ .filter((e) => e.event.type === "artifact.created")
119
+ .map((e) => (e.event.type === "artifact.created" ? e.event.hash : "")));
120
+ if (!artifactHashes.has(receipt.workspaceOut.diffHash)) {
121
+ problems.push("workspaceOut.diffHash has no matching artifact.created event");
122
+ }
123
+ }
124
+ return { ok: problems.length === 0, problems };
125
+ }
126
+ /**
127
+ * Fully offline verification of a receipt bundle. Trusts nothing but the
128
+ * keys embedded in the bundle, which callers should pin or resolve from
129
+ * the org's published key manifest.
130
+ */
131
+ function verifyReceiptBundleUnchecked(bundle) {
132
+ const problems = [];
133
+ const { contract, receipt, events, keys } = bundle;
134
+ if (!verifyContractSignature(contract, "plane", (keyId) => keyId === keyIdFromPublicPem(keys.planePublicKeyPem)
135
+ ? keys.planePublicKeyPem
136
+ : undefined)) {
137
+ problems.push("contract plane signature invalid");
138
+ }
139
+ problems.push(...verifyRunnerReceipt({
140
+ contract,
141
+ receipt,
142
+ events,
143
+ runnerPublicKeyPem: keys.runnerPublicKeyPem
144
+ }).problems);
145
+ if (!verifyReceiptSignature(receipt, "plane", keys.planePublicKeyPem)) {
146
+ problems.push("receipt plane countersignature invalid");
147
+ }
148
+ return { ok: problems.length === 0, problems };
149
+ }
150
+ export function verifyReceiptBundle(bundle) {
151
+ try {
152
+ return verifyReceiptBundleUnchecked(bundle);
153
+ }
154
+ catch (error) {
155
+ return {
156
+ ok: false,
157
+ problems: [
158
+ `bundle is malformed: ${error instanceof Error ? error.message : String(error)}`
159
+ ]
160
+ };
161
+ }
162
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,213 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { artifactHash, assertArtifactRefV1, assertBenchmarkTaskRecordV1, assertEnsembleReceiptV1, assertHarnessCandidateRecordV1, assertHarnessRunRequestV1, assertHarnessRunResultV1, assertJudgeSynthesisRecordV1, assertModelCallRecordV1, assertModelFusionRecord, assertToolCallPlanV1, assertToolExecutionRecordV1, hashCanonicalSha256, executeHarnessTask, MODEL_FUSION_SCHEMA_BUNDLE_HASH, MODEL_FUSION_OPENAPI_SOURCE_HASH, MODEL_FUSION_SCHEMA_NAMES, requestHash, responseHash, schemaBundleHash, sha256PrefixedHex } from "../index.js";
7
+ const FIXTURE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "src", "fixtures", "model-fusion-contract");
8
+ const SCHEMAS = [
9
+ "model-call-record.v1",
10
+ "harness-run-request.v1",
11
+ "harness-run-result.v1",
12
+ "harness-candidate-record.v1",
13
+ "judge-synthesis-record.v1",
14
+ "benchmark-task-record.v1",
15
+ "artifact-ref.v1",
16
+ "tool-call-plan.v1",
17
+ "tool-execution-record.v1",
18
+ "ensemble-receipt.v1"
19
+ ];
20
+ const VALIDATORS = {
21
+ "model-call-record.v1": assertModelCallRecordV1,
22
+ "harness-run-request.v1": assertHarnessRunRequestV1,
23
+ "harness-run-result.v1": assertHarnessRunResultV1,
24
+ "harness-candidate-record.v1": assertHarnessCandidateRecordV1,
25
+ "judge-synthesis-record.v1": assertJudgeSynthesisRecordV1,
26
+ "benchmark-task-record.v1": assertBenchmarkTaskRecordV1,
27
+ "artifact-ref.v1": assertArtifactRefV1,
28
+ "tool-call-plan.v1": assertToolCallPlanV1,
29
+ "tool-execution-record.v1": assertToolExecutionRecordV1,
30
+ "ensemble-receipt.v1": assertEnsembleReceiptV1
31
+ };
32
+ function readFixture(schema, variant) {
33
+ return JSON.parse(readFileSync(join(FIXTURE_ROOT, schema, `${variant}.json`), "utf8"));
34
+ }
35
+ test("model-fusion schema constants include the MF-02 record set", () => {
36
+ assert.deepEqual([...MODEL_FUSION_SCHEMA_NAMES], [...SCHEMAS]);
37
+ });
38
+ test("model-fusion fixtures carry the exported schema bundle hash", () => {
39
+ assert.match(MODEL_FUSION_SCHEMA_BUNDLE_HASH, /^sha256:[0-9a-f]{64}$/);
40
+ for (const schema of SCHEMAS) {
41
+ for (const variant of ["minimal", "realistic"]) {
42
+ const fixture = readFixture(schema, variant);
43
+ assert.equal(fixture.schema_bundle_hash, MODEL_FUSION_SCHEMA_BUNDLE_HASH);
44
+ }
45
+ }
46
+ });
47
+ test("specific validators accept copied MF-00 minimal and realistic fixtures", () => {
48
+ for (const schema of SCHEMAS) {
49
+ const validate = VALIDATORS[schema];
50
+ validate(readFixture(schema, "minimal"));
51
+ validate(readFixture(schema, "realistic"));
52
+ }
53
+ });
54
+ test("union validator accepts every copied model-fusion fixture", () => {
55
+ for (const schema of SCHEMAS) {
56
+ assertModelFusionRecord(readFixture(schema, "minimal"));
57
+ assertModelFusionRecord(readFixture(schema, "realistic"));
58
+ }
59
+ });
60
+ test("model-fusion validators reject wrong schema and missing required fields", () => {
61
+ const call = readFixture("model-call-record.v1", "minimal");
62
+ assertModelCallRecordV1(call);
63
+ assert.throws(() => assertModelCallRecordV1({ ...call, schema: "artifact-ref.v1" }), /schema must be model-call-record.v1/);
64
+ const { call_id: _callId, ...missingCallId } = call;
65
+ assert.throws(() => assertModelCallRecordV1(missingCallId), /call_id/);
66
+ assert.throws(() => assertModelFusionRecord({ schema: "unknown.v1" }), /unsupported/);
67
+ });
68
+ test("model-fusion validators reject bad enum and hash values", () => {
69
+ const plan = readFixture("tool-call-plan.v1", "minimal");
70
+ assert.throws(() => assertToolCallPlanV1({ ...plan, status: "done" }), /status/);
71
+ assert.throws(() => assertToolCallPlanV1({ ...plan, arguments_hash: "not-a-hash" }), /arguments_hash/);
72
+ });
73
+ test("model-fusion validators reject unsupported fields", () => {
74
+ const call = readFixture("model-call-record.v1", "minimal");
75
+ assert.throws(() => assertModelCallRecordV1({ ...call, extra_field: true }), /unsupported field/);
76
+ const messages = call.messages;
77
+ assert.throws(() => assertModelCallRecordV1({
78
+ ...call,
79
+ messages: [{ ...messages[0], hidden: true }]
80
+ }), /unsupported field/);
81
+ const result = readFixture("harness-run-result.v1", "realistic");
82
+ const artifacts = result.artifacts;
83
+ assert.throws(() => assertHarnessRunResultV1({
84
+ ...result,
85
+ artifacts: [{ ...artifacts[0], schema: "artifact-ref.v1" }]
86
+ }), /unsupported field/);
87
+ });
88
+ test("harness candidate metadata accepts nested microVM hardening evidence", () => {
89
+ const candidate = readFixture("harness-candidate-record.v1", "minimal");
90
+ const withHardeningMetadata = {
91
+ ...candidate,
92
+ metadata: {
93
+ hardening: {
94
+ requested_isolation: "microvm",
95
+ actual_isolation: "vercel-sandbox",
96
+ provider: "vercel-sandbox",
97
+ runtime: {
98
+ engine: "firecracker",
99
+ node_version: "22.x"
100
+ },
101
+ sandbox: {
102
+ sandbox_id: "sbx_microvm_fixture",
103
+ snapshot_id: "snap_microvm_fixture",
104
+ persistent: false
105
+ },
106
+ network: {
107
+ default_deny: true,
108
+ allowed_hosts: []
109
+ },
110
+ cleanup: {
111
+ status: "succeeded",
112
+ duration_ms: 17
113
+ },
114
+ secrets: {
115
+ mounted: false
116
+ }
117
+ }
118
+ }
119
+ };
120
+ assertHarnessCandidateRecordV1(withHardeningMetadata);
121
+ assertModelFusionRecord(withHardeningMetadata);
122
+ assert.throws(() => assertHarnessCandidateRecordV1({
123
+ ...candidate,
124
+ microvm: withHardeningMetadata.metadata
125
+ }), /unsupported field/);
126
+ });
127
+ test("harness candidate metadata accepts disclosure joins without top-level schema changes", () => {
128
+ const candidate = readFixture("harness-candidate-record.v1", "minimal");
129
+ const withDisclosureMetadata = {
130
+ ...candidate,
131
+ metadata: {
132
+ disclosures: [
133
+ {
134
+ candidate_id: "candidate_a",
135
+ tool_call_id: "tool_call_readme",
136
+ plan_id: "tool_plan_readme",
137
+ execution_id: "tool_exec_readme",
138
+ run_id: "run_secret_disclosure",
139
+ content_hash: "sha256:" + "a".repeat(64),
140
+ data_class: "session-log",
141
+ direction: "out",
142
+ policy_id: "policy_readonly",
143
+ environment_id: "env_local",
144
+ secret_names: ["API_TOKEN"],
145
+ injected_env_names: ["API_TOKEN"],
146
+ redaction_status: "redacted"
147
+ }
148
+ ]
149
+ }
150
+ };
151
+ assertHarnessCandidateRecordV1(withDisclosureMetadata);
152
+ assertModelFusionRecord(withDisclosureMetadata);
153
+ assert.throws(() => assertHarnessCandidateRecordV1({
154
+ ...candidate,
155
+ disclosures: withDisclosureMetadata.metadata
156
+ }), /unsupported field/);
157
+ });
158
+ test("model-fusion hash helpers return sha256-prefixed hashes", () => {
159
+ assert.match(sha256PrefixedHex("hello"), /^sha256:[0-9a-f]{64}$/);
160
+ assert.match(artifactHash(Buffer.from("artifact")), /^sha256:[0-9a-f]{64}$/);
161
+ assert.match(requestHash({ b: 2, a: 1 }), /^sha256:[0-9a-f]{64}$/);
162
+ assert.equal(requestHash({ b: 2, a: 1 }), responseHash({ a: 1, b: 2 }));
163
+ assert.equal(hashCanonicalSha256({ z: true }), requestHash({ z: true }));
164
+ assert.match(schemaBundleHash({
165
+ "b.schema.json": { b: true },
166
+ "a.schema.json": { a: true }
167
+ }), /^sha256:[0-9a-f]{64}$/);
168
+ });
169
+ test("model-fusion exports are available from the protocol package entrypoint", () => {
170
+ assert.equal(typeof assertModelFusionRecord, "function");
171
+ assert.equal(typeof assertHarnessRunRequestV1, "function");
172
+ assert.equal(typeof requestHash, "function");
173
+ });
174
+ test("generated OpenAPI client and service models are exported", async () => {
175
+ assert.match(MODEL_FUSION_OPENAPI_SOURCE_HASH, /^sha256:[0-9a-f]{64}$/);
176
+ const request = {
177
+ request_id: "req_generated",
178
+ task_id: "task_generated",
179
+ source_repo: "handoffkit",
180
+ base_git_sha: "0".repeat(40),
181
+ harness_kind: "generic",
182
+ prompt_hash: requestHash("generated"),
183
+ allowed_tools: ["read_file"],
184
+ side_effects: "read_only",
185
+ harness_run_request: {
186
+ schema: "harness-run-request.v1",
187
+ schema_version: "v1",
188
+ schema_bundle_hash: MODEL_FUSION_SCHEMA_BUNDLE_HASH,
189
+ persisted_json: {}
190
+ }
191
+ };
192
+ const result = await executeHarnessTask({
193
+ baseUrl: "https://executor.example",
194
+ fetch: async (url, init) => {
195
+ assert.equal(String(url), "https://executor.example/v1/harness-executions");
196
+ assert.equal(init?.method, "POST");
197
+ assert.match(String(init?.body), /req_generated/);
198
+ return new Response(JSON.stringify({
199
+ request_id: request.request_id,
200
+ result_id: "result_generated",
201
+ status: "succeeded",
202
+ candidate_ids: [],
203
+ harness_run_result: {
204
+ schema: "harness-run-result.v1",
205
+ schema_version: "v1",
206
+ schema_bundle_hash: MODEL_FUSION_SCHEMA_BUNDLE_HASH,
207
+ persisted_json: {}
208
+ }
209
+ }), { status: 200, headers: { "content-type": "application/json" } });
210
+ }
211
+ }, request);
212
+ assert.equal(result.result_id, "result_generated");
213
+ });
@@ -0,0 +1 @@
1
+ export {};