@opennodes/registry 0.1.2 → 0.1.3
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/package.json +1 -1
- package/src/server.js +21 -1
- package/src/stageb.js +7 -2
- package/src/stagec.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennodes/registry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OpenNodes (ONP) reference registry: node registration with proof-of-resource qualification (Stage A/B/C), transparent ranking, enforceable estimates, catalog importers (OpenRouter, Hugging Face, models.dev, Ollama), MCP surface, and the web app (catalog, chat, advisor, Node Builder)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/server.js
CHANGED
|
@@ -100,6 +100,22 @@ export async function startRegistry({
|
|
|
100
100
|
return { card, sigOk };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Re-index a registered node's card when its origin serves a newer revision (restart,
|
|
105
|
+
* reprice, config change). Probes pin the revision they hold; without this a restarted
|
|
106
|
+
* node answers 409 to every probe and would look like an identity mismatch.
|
|
107
|
+
*/
|
|
108
|
+
async function refreshCard(nodeId) {
|
|
109
|
+
try {
|
|
110
|
+
const node = await store.getNode(nodeId);
|
|
111
|
+
if (!node?.card_url || node.source === 'import') return;
|
|
112
|
+
const current = await store.latestCard(nodeId);
|
|
113
|
+
const fresh = await fetchCard(node.card_url);
|
|
114
|
+
if (current && fresh.revision === current.revision) return;
|
|
115
|
+
await indexCard(nodeId, node.card_url);
|
|
116
|
+
} catch { /* unreachable or invalid: probes will record the failure themselves */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
103
119
|
const app = createApp([
|
|
104
120
|
['POST', '/v0/nodes', async (req, res) => {
|
|
105
121
|
const { card_url } = await readJson(req);
|
|
@@ -172,8 +188,12 @@ export async function startRegistry({
|
|
|
172
188
|
if (node.state !== 'community' && node.state !== 'verified') {
|
|
173
189
|
return problem(res, 409, 'wrong-state', `Stage B requires community tier, node is ${node.state}`);
|
|
174
190
|
}
|
|
191
|
+
await refreshCard(params.id);
|
|
175
192
|
const card = await store.latestCard(params.id);
|
|
176
193
|
const stageB = await runStageB(store, params.id, card, { safeFetch, capacity: stageBCapacity });
|
|
194
|
+
if (stageB.stale) {
|
|
195
|
+
return problem(res, 409, 'stale-card', 'the node changed its card revision during the battery; retry');
|
|
196
|
+
}
|
|
177
197
|
if (stageB.identityFailed) {
|
|
178
198
|
await store.setState(params.id, 'disputed', 'Stage B identity fingerprint mismatch');
|
|
179
199
|
} else if (stageB.ok) {
|
|
@@ -372,7 +392,7 @@ export async function startRegistry({
|
|
|
372
392
|
...exportRoutes(store, registryKeys),
|
|
373
393
|
], { cors: true }); // public read API: third-party web clients may call it directly
|
|
374
394
|
|
|
375
|
-
const stageCHandle = stageC ? startStageC(store, { safeFetch, ...stageC }) : null;
|
|
395
|
+
const stageCHandle = stageC ? startStageC(store, { safeFetch, refreshCard, ...stageC }) : null;
|
|
376
396
|
const reimportHandle = reimport ? startReimport(store, { safeFetch, ...reimport }) : null;
|
|
377
397
|
const actualPort = await listen(app, port, host);
|
|
378
398
|
return {
|
package/src/stageb.js
CHANGED
|
@@ -52,13 +52,14 @@ export async function captureFingerprints(safeFetch, card, offering) {
|
|
|
52
52
|
|
|
53
53
|
/** DS-ADM-02: identity check against stored references. */
|
|
54
54
|
async function probeIdentity(safeFetch, card, offering, refs) {
|
|
55
|
-
let matches = 0;
|
|
55
|
+
let matches = 0, stale = false;
|
|
56
56
|
for (const ref of refs) {
|
|
57
57
|
const { status, text } = await chatOnce(safeFetch, card, offering, ref.prompt);
|
|
58
|
+
if (status === 409) stale = true; // pinned revision is no longer current: not evidence about identity
|
|
58
59
|
if (status === 200 && text !== null && sha256(text) === ref.expected_hash) matches++;
|
|
59
60
|
}
|
|
60
61
|
// Deterministic battery: require all prompts to match the reference distribution.
|
|
61
|
-
return { ok: matches === refs.length, matches, total: refs.length };
|
|
62
|
+
return { ok: matches === refs.length, matches, total: refs.length, ...(stale ? { stale: true } : {}) };
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
/** A needle counts as retrieved if the reply carries the full token or just its digits (small models drop the prefix). */
|
|
@@ -145,6 +146,10 @@ export async function runStageB(store, nodeId, card, { safeFetch, capacity = {}
|
|
|
145
146
|
const refs = await store.getFingerprints(offering.model.artifact);
|
|
146
147
|
if (refs.length) {
|
|
147
148
|
verdict.identity = await probeIdentity(safeFetch, card, offering, refs);
|
|
149
|
+
if (verdict.identity.stale) {
|
|
150
|
+
// The node rejected our pinned revision (409). Abort without judging: the caller re-indexes and retries.
|
|
151
|
+
return { ok: false, identityFailed: false, stale: true, verdicts: [...verdicts, verdict] };
|
|
152
|
+
}
|
|
148
153
|
await store.recordProbe(nodeId, offering.offering_id, 'B', 'identity', verdict.identity.ok,
|
|
149
154
|
`${verdict.identity.matches}/${verdict.identity.total} reference matches`);
|
|
150
155
|
} else {
|
package/src/stagec.js
CHANGED
|
@@ -9,6 +9,7 @@ const OBSERVED_STATES = ['community', 'verified', 'attested'];
|
|
|
9
9
|
|
|
10
10
|
export function startStageC(store, {
|
|
11
11
|
safeFetch,
|
|
12
|
+
refreshCard = null, // re-index a node's card when its revision moved (restart / reprice)
|
|
12
13
|
livenessIntervalMs = 300_000, // spec: <=5 min; tests shorten this
|
|
13
14
|
blindIntervalMs = 3_600_000,
|
|
14
15
|
failThreshold = 3,
|
|
@@ -47,6 +48,7 @@ export function startStageC(store, {
|
|
|
47
48
|
async function blindSweep() {
|
|
48
49
|
for (const node of await store.allNodes()) {
|
|
49
50
|
if (!OBSERVED_STATES.includes(node.state)) continue;
|
|
51
|
+
await refreshCard?.(node.id);
|
|
50
52
|
const card = await store.latestCard(node.id);
|
|
51
53
|
if (!card) continue;
|
|
52
54
|
const chatOfferings = card.offerings.filter((o) => o.binding.profile === 'onp.openai.chat/v1');
|
|
@@ -66,6 +68,7 @@ export function startStageC(store, {
|
|
|
66
68
|
max_tokens: 32,
|
|
67
69
|
}),
|
|
68
70
|
}, 30_000);
|
|
71
|
+
if (status === 409) { await refreshCard?.(node.id); continue; } // stale pin, not a failure of the node
|
|
69
72
|
const usage = body?.usage;
|
|
70
73
|
const ok = status === 200 && Number.isFinite(usage?.prompt_tokens) && Number.isFinite(usage?.completion_tokens);
|
|
71
74
|
await store.recordProbe(node.id, offering.offering_id, 'C', 'blind', ok,
|