@muretai/agent-entry 1.3.0 → 1.5.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/conformance/run.mjs +119 -0
- package/conformance/vectors.json +637 -0
- package/examples/server.mjs +24 -1
- package/muretai-agent-entry.mjs +517 -17
- package/package.json +3 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* conformance/run.mjs — check an Agent Entry implementation against the golden vectors.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS SHIPS IN THE PACKAGE. A conformance suite that lives on a website is a suite
|
|
6
|
+
* with an uptime requirement, and until now this one was worse than that: the vectors sat
|
|
7
|
+
* in a git checkout that `npm install` never delivers, so "run the suite" meant "send us
|
|
8
|
+
* your implementation and we will run it". Everything needed to hold this code to its own
|
|
9
|
+
* contract is now in the tarball: `npm test`, no network, no dependencies, no account.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT CHECKS, AND WHY BOTH HALVES ARE HERE. The positive half proves this build
|
|
12
|
+
* produces the same BYTES as every other implementation — canonical JSON, did:key, the six
|
|
13
|
+
* signed fields. The negative half proves it REFUSES what it must, and it is the half that
|
|
14
|
+
* catches the failure nobody notices: an implementation that verifies nothing passes every
|
|
15
|
+
* positive vector in the file. A drift in either direction is silent on the wire — nothing
|
|
16
|
+
* throws, signatures simply stop verifying for everyone else.
|
|
17
|
+
*
|
|
18
|
+
* Run: node conformance/run.mjs (from the package root)
|
|
19
|
+
* npm test
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
canonicalJSON, didFromPublicKeyHex, publicKeyFromSeedHex, signingPayload,
|
|
28
|
+
signEnvelope, verifyEnvelope,
|
|
29
|
+
} from '../muretai-agent-entry.mjs';
|
|
30
|
+
|
|
31
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const vectors = JSON.parse(readFileSync(join(HERE, 'vectors.json'), 'utf8'));
|
|
33
|
+
|
|
34
|
+
let pass = 0;
|
|
35
|
+
const failures = [];
|
|
36
|
+
|
|
37
|
+
function check(ok, label, detail) {
|
|
38
|
+
if (ok) { pass += 1; return true; }
|
|
39
|
+
failures.push(detail ? `${label}\n ${detail}` : label);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------- canonical JSON
|
|
44
|
+
for (const v of vectors.canonical) {
|
|
45
|
+
let got;
|
|
46
|
+
try { got = canonicalJSON(v.payload); } catch (e) { got = `THREW: ${e.message}`; }
|
|
47
|
+
check(got === v.canonical, `canonical/${v.name}`,
|
|
48
|
+
got === v.canonical ? '' : `want ${JSON.stringify(v.canonical)}\n got ${JSON.stringify(got)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// `numberHazards` is DELIBERATELY NOT EXECUTED, and reading it is the point. Every case
|
|
52
|
+
// there is a value whose canonical bytes differ between languages, so asserting either
|
|
53
|
+
// spelling would be asserting one runtime's float formatting — the opposite of the
|
|
54
|
+
// contract. The rule it carries is a SIGNER discipline (`signMustNotEmit`), not a
|
|
55
|
+
// canonicaliser output: never sign a payload containing one, because the bytes you produce
|
|
56
|
+
// will only verify where they were produced. An agent entry meets it for free — the only
|
|
57
|
+
// number among the six signed fields is `timestamp`, and integer epoch seconds is the
|
|
58
|
+
// contract.
|
|
59
|
+
const hazards = vectors.numberHazards?.length ?? 0;
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------- did:key
|
|
62
|
+
for (const v of vectors.did) {
|
|
63
|
+
if (v.curve !== 'ed25519') continue; // p256 did:key is not an envelope signer
|
|
64
|
+
let got;
|
|
65
|
+
try { got = didFromPublicKeyHex(v.publicHex); } catch (e) { got = `THREW: ${e.message}`; }
|
|
66
|
+
check(got === v.did, `did/${v.publicHex.slice(0, 12)}…`,
|
|
67
|
+
got === v.did ? '' : `want ${v.did}\n got ${got}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------- the six signed fields
|
|
71
|
+
for (const v of vectors.envelope) {
|
|
72
|
+
const fields = { from: v.from, to: v.to, messageId: v.messageId,
|
|
73
|
+
contextId: v.contextId ?? null, timestamp: v.timestamp, text: v.text };
|
|
74
|
+
let got;
|
|
75
|
+
try { got = signingPayload(fields); } catch (e) { got = `THREW: ${e.message}`; }
|
|
76
|
+
check(got === v.signingPayload, `envelope/${v.name}`,
|
|
77
|
+
got === v.signingPayload ? '' : `want ${JSON.stringify(v.signingPayload)}\n got ${JSON.stringify(got)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A signature this build makes must verify in this build. Round-tripping is the weakest
|
|
81
|
+
// possible claim on its own — it only says the code agrees with itself — which is exactly
|
|
82
|
+
// why the byte checks above and the refusals below are not optional.
|
|
83
|
+
{
|
|
84
|
+
const seed = '11'.repeat(32);
|
|
85
|
+
const from = didFromPublicKeyHex(publicKeyFromSeedHex(seed));
|
|
86
|
+
const fields = { from, to: from, messageId: 'm1', contextId: null,
|
|
87
|
+
timestamp: 1752451200, text: 'round trip' };
|
|
88
|
+
const sig = signEnvelope(seed, fields);
|
|
89
|
+
check(verifyEnvelope({ ...fields, sig }, { recipientDid: from }), 'envelope/round-trip');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------- the refusals
|
|
93
|
+
// The half that catches an implementation which verifies nothing.
|
|
94
|
+
for (const v of vectors.reject.message) {
|
|
95
|
+
const fields = { from: v.from, to: v.to, messageId: v.messageId,
|
|
96
|
+
contextId: v.contextId ?? null, timestamp: v.timestamp,
|
|
97
|
+
text: v.text, sig: v.sig };
|
|
98
|
+
let accepted;
|
|
99
|
+
try {
|
|
100
|
+
accepted = verifyEnvelope(fields, { recipientDid: v.recipientDid ?? v.to });
|
|
101
|
+
} catch {
|
|
102
|
+
accepted = false; // refusing by throwing is still refusing
|
|
103
|
+
}
|
|
104
|
+
check(accepted === false, `reject/${v.name}`,
|
|
105
|
+
accepted === false ? '' : `ACCEPTED a message it must refuse — ${v.why || ''}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------- verdict
|
|
109
|
+
console.log(`\n${vectors.note}\n`);
|
|
110
|
+
if (failures.length) {
|
|
111
|
+
console.log(`FAILED — ${failures.length} of ${pass + failures.length} checks:\n`);
|
|
112
|
+
for (const f of failures) console.log(` ✗ ${f}`);
|
|
113
|
+
console.log('\nA mismatch here is not cosmetic: these bytes are what every other');
|
|
114
|
+
console.log('implementation signs and verifies.\n');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
console.log(`OK — ${pass} checks: the bytes match, and every case that must be refused was.`);
|
|
118
|
+
console.log(` (${hazards} numberHazards read, not executed — see the comment in this file:`);
|
|
119
|
+
console.log(` they are a SIGNER rule, not bytes any single runtime can be held to.)\n`);
|