@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.
- package/dist/api.d.ts +106 -0
- package/dist/api.js +6 -0
- package/dist/chain.d.ts +14 -0
- package/dist/chain.js +49 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +36 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +32 -0
- package/dist/execution.d.ts +67 -0
- package/dist/execution.js +27 -0
- package/dist/generated/model-fusion-openapi.d.ts +44 -0
- package/dist/generated/model-fusion-openapi.js +23 -0
- package/dist/hash.d.ts +10 -0
- package/dist/hash.js +31 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +30 -0
- package/dist/jcs.d.ts +14 -0
- package/dist/jcs.js +49 -0
- package/dist/keys.d.ts +14 -0
- package/dist/keys.js +36 -0
- package/dist/model-fusion.d.ts +167 -0
- package/dist/model-fusion.js +596 -0
- package/dist/receipt-story.d.ts +27 -0
- package/dist/receipt-story.js +127 -0
- package/dist/receipt.d.ts +20 -0
- package/dist/receipt.js +162 -0
- package/dist/test/model-fusion.test.d.ts +1 -0
- package/dist/test/model-fusion.test.js +213 -0
- package/dist/test/protocol.test.d.ts +1 -0
- package/dist/test/protocol.test.js +240 -0
- package/dist/test/tool-executor.test.d.ts +1 -0
- package/dist/test/tool-executor.test.js +86 -0
- package/dist/test/trace.test.d.ts +1 -0
- package/dist/test/trace.test.js +75 -0
- package/dist/tool-executor.d.ts +58 -0
- package/dist/tool-executor.js +80 -0
- package/dist/trace.d.ts +119 -0
- package/dist/trace.js +248 -0
- package/dist/types.d.ts +375 -0
- package/dist/types.js +14 -0
- package/dist/validators.d.ts +7 -0
- package/dist/validators.js +32 -0
- package/dist/vocabulary.d.ts +12 -0
- package/dist/vocabulary.js +79 -0
- 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;
|
package/dist/receipt.js
ADDED
|
@@ -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 {};
|