mycomesh-consumer 0.9.7 → 0.9.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mycomesh-consumer",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "MycoMesh V11 Consumer: a custodied deposit, relay-blind encrypted requests to any Provider, and a local OpenAI-compatible endpoint.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/consumer.mjs CHANGED
@@ -6,7 +6,7 @@ import { dirname, resolve } from "node:path";
6
6
  import { rootCertificates } from "node:tls";
7
7
  import { addressOf, encodeCall } from "./eip712.mjs";
8
8
  import { rpcCall } from "./chain.mjs";
9
- import { openDelta, openResponse, outputText, prepareRequest, verifiedProvider } from "./protocol.mjs";
9
+ import { openDelta, openResponse, outputText, planRequest, prepareRequest, verifiedProvider } from "./protocol.mjs";
10
10
  import { recordRequest } from "./disputes.mjs";
11
11
  import { chatStream, responseStream } from "./sse.mjs";
12
12
  import { consoleRoutes, localRequestAllowed } from "./console.mjs";
@@ -16,9 +16,35 @@ import { hashApiKey } from "./tenants.mjs";
16
16
  import { anthropicError, anthropicStream, fromAnthropic, fromGemini, geminiError, geminiRoute, geminiStream,
17
17
  toAnthropic, toGemini } from "./dialects.mjs";
18
18
 
19
- const PROVIDER_CACHE_MS = 30_000;
20
- const RELAY_CACHE_MS = 300_000;
21
- const REPUTATION_CACHE_MS = 600_000;
19
+ // Discovery is served from cache and refreshed in the background once it is older than its fresh period; only
20
+ // data older than its usable period (or none yet) makes a request wait for it.
21
+ const PROVIDER_CACHE_MS = [30_000, 300_000];
22
+ const RELAY_CACHE_MS = [300_000, 3_600_000];
23
+ const REPUTATION_CACHE_MS = [600_000, 3_600_000];
24
+ const REPUTATION_WAIT_MS = 3_000; // a first request ranks without reputation rather than wait longer for it
25
+ const WARM_EVERY_MS = 20_000; // serve: keep discovery fresh and a connection to every Relay open
26
+
27
+ /** A value loaded once at a time, served stale while it is refreshed. */
28
+ class Cached {
29
+ constructor(load, [freshMs, usableMs]) { Object.assign(this, { load, freshMs, usableMs, at: 0, value: undefined, pending: null }); }
30
+
31
+ get() {
32
+ const age = Date.now() - this.at;
33
+ if (this.value !== undefined && age < this.freshMs) return Promise.resolve(this.value);
34
+ if (this.value !== undefined && age < this.usableMs) { this.refresh().catch(() => {}); return Promise.resolve(this.value); }
35
+ return this.refresh();
36
+ }
37
+
38
+ refresh() {
39
+ this.pending ||= this.load().then((value) => { Object.assign(this, { value, at: Date.now() }); return value; })
40
+ .finally(() => { this.pending = null; });
41
+ return this.pending;
42
+ }
43
+ }
44
+
45
+ // Consumers of one network (a node and its tenants) share what they discover.
46
+ const DISCOVERY = new WeakMap();
47
+ const VERIFIED = new WeakMap(); // descriptor -> the times its verification holds
22
48
  const MAX_BODY = 16 * 1024 * 1024;
23
49
 
24
50
  export function loadNetwork(path) {
@@ -29,30 +55,63 @@ export function loadNetwork(path) {
29
55
  }
30
56
 
31
57
  /**
32
- * A fresh connection per request, through an agent of its own: a reused keep-alive socket that the Relay's
33
- * edge has just closed fails with ECONNRESET after the request may have been sent, which can only be
34
- * reported as outcome unknown. (Only an agent's own createConnection is honoured, hence the pinned one here.)
58
+ * Kept-alive connections, one agent per Relay origin (and pin): a request to a Relay this Consumer talked to in
59
+ * the last half minute skips the TCP and TLS handshakes (several round trips across the world). Idle sockets
60
+ * are dropped after `keepAlive.idleMs`, well inside the Relay edge's own keep-alive (nginx: 75 s), and a request
61
+ * whose reused socket turns out to have been closed by the Relay before any reply is sent again on a fresh
62
+ * one: a closed idle connection never carried it, and the Relay and Provider refuse a settlement key twice
63
+ * anyway. (Only an agent's own createConnection is honoured, hence the pinned one here.)
35
64
  */
36
- async function tlsOptions(target, ca, pin) {
37
- if (target.protocol !== "https:") return { agent: new HttpAgent({ keepAlive: false }) };
38
- const agent = new HttpsAgent({ keepAlive: false });
39
- if (pin) {
40
- agent.createConnection = pinnedOptions(target.href, pin).createConnection;
41
- return { agent };
65
+ export const keepAlive = { idleMs: 30_000 };
66
+ const AGENTS = new Map();
67
+
68
+ function tlsOptions(target, ca, pin) {
69
+ const key = `${target.protocol}//${target.host}#${pin || ""}#${ca ? "ca" : ""}`;
70
+ let options = AGENTS.get(key);
71
+ if (!options) {
72
+ const settings = { keepAlive: true, timeout: keepAlive.idleMs, scheduling: "lifo" };
73
+ if (target.protocol !== "https:") options = { agent: new HttpAgent(settings) };
74
+ else {
75
+ const agent = new HttpsAgent(settings);
76
+ if (pin) agent.createConnection = pinnedOptions(target.href, pin).createConnection;
77
+ options = pin || !ca ? { agent } : { agent, ca: [...rootCertificates, ca] };
78
+ }
79
+ AGENTS.set(key, options);
42
80
  }
43
- return ca ? { agent, ca: [...rootCertificates, ca] } : { agent };
81
+ return options;
82
+ }
83
+
84
+ /** Drop every kept-alive connection (tests, shutdown). */
85
+ export function closeConnections() {
86
+ for (const { agent } of AGENTS.values()) agent.destroy();
87
+ AGENTS.clear();
88
+ }
89
+
90
+ const STALE = new Set(["ECONNRESET", "EPIPE", "ECONNABORTED"]);
91
+
92
+ /** Send once; again if a reused connection was already closed by the Relay before anything came back. */
93
+ function withFreshRetry(send) {
94
+ return new Promise((resolvePromise, reject) => {
95
+ const attempt = (tries) => send((error, req, answered) => {
96
+ if (error && req?.reusedSocket && !answered && STALE.has(error.code) && tries < 3) return attempt(tries + 1);
97
+ return error ? reject(error) : undefined;
98
+ }, resolvePromise);
99
+ attempt(0);
100
+ });
44
101
  }
45
102
 
46
103
  export async function httpJson(url, { method = "GET", body, ca, pin, timeoutMs = 330_000 } = {}) {
47
104
  const target = new URL(url);
48
105
  const data = body === undefined ? undefined : Buffer.from(JSON.stringify(body));
49
106
  const send = target.protocol === "https:" ? httpsRequest : httpRequest;
50
- const tls = await tlsOptions(target, ca, pin);
51
- return new Promise((resolvePromise, reject) => {
107
+ const tls = tlsOptions(target, ca, pin);
108
+ return withFreshRetry((fail, resolvePromise) => {
109
+ let answered = false;
52
110
  const req = send(target, {
53
111
  method, headers: { accept: "application/json", ...(data ? { "content-type": "application/json", "content-length": data.length } : {}) },
54
112
  ...tls,
55
113
  }, (res) => {
114
+ answered = true;
56
115
  const chunks = [];
57
116
  let size = 0;
58
117
  res.on("data", (chunk) => {
@@ -62,14 +121,14 @@ export async function httpJson(url, { method = "GET", body, ca, pin, timeoutMs =
62
121
  });
63
122
  res.on("end", () => {
64
123
  try { resolvePromise({ status: res.statusCode, body: JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}") }); }
65
- catch (error) { reject(error); }
124
+ catch (error) { fail(error, req, true); }
66
125
  });
67
126
  });
68
127
  // A wall-clock deadline, not a socket idle timeout: keep-alive sockets are reused, and an idle timeout
69
128
  // left on one by an earlier short request (a 10 s /providers call) would later cut a long answer.
70
129
  const timer = setTimeout(() => req.destroy(new Error(`request to ${target.host}${target.pathname} timed out after ${timeoutMs / 1000} s`)), timeoutMs);
71
130
  req.on("close", () => clearTimeout(timer));
72
- req.on("error", reject);
131
+ req.on("error", (error) => fail(error, req, answered));
73
132
  if (data) req.write(data);
74
133
  req.end();
75
134
  });
@@ -80,13 +139,15 @@ export async function httpNdjson(url, { body, ca, pin, timeoutMs = 340_000, onLi
80
139
  const target = new URL(url);
81
140
  const data = Buffer.from(JSON.stringify(body));
82
141
  const send = target.protocol === "https:" ? httpsRequest : httpRequest;
83
- const tls = await tlsOptions(target, ca, pin);
84
- return new Promise((resolvePromise, reject) => {
142
+ const tls = tlsOptions(target, ca, pin);
143
+ return withFreshRetry((fail, resolvePromise) => {
144
+ let answered = false;
85
145
  const req = send(target, {
86
146
  method: "POST",
87
147
  headers: { accept: "application/x-ndjson", "content-type": "application/json", "content-length": data.length },
88
148
  ...tls,
89
149
  }, (res) => {
150
+ answered = true;
90
151
  const streaming = res.statusCode === 200 && String(res.headers["content-type"] || "").includes("ndjson");
91
152
  let buffered = "";
92
153
  const chunks = [];
@@ -105,14 +166,14 @@ export async function httpNdjson(url, { body, ca, pin, timeoutMs = 340_000, onLi
105
166
  });
106
167
  res.on("end", () => {
107
168
  if (streaming) return resolvePromise({ status: 200, streamed: true });
108
- try { resolvePromise({ status: res.statusCode, body: JSON.parse(chunks.join("") || "{}") }); } catch (error) { reject(error); }
169
+ try { resolvePromise({ status: res.statusCode, body: JSON.parse(chunks.join("") || "{}") }); } catch (error) { fail(error, req, true); }
109
170
  });
110
171
  });
111
172
  // A wall-clock deadline, not a socket idle timeout: keep-alive sockets are reused, and an idle timeout
112
173
  // left on one by an earlier short request (a 10 s /providers call) would later cut a long answer.
113
174
  const timer = setTimeout(() => req.destroy(new Error(`request to ${target.host}${target.pathname} timed out after ${timeoutMs / 1000} s`)), timeoutMs);
114
175
  req.on("close", () => clearTimeout(timer));
115
- req.on("error", reject);
176
+ req.on("error", (error) => fail(error, req, answered));
116
177
  req.end(data);
117
178
  });
118
179
  }
@@ -152,32 +213,55 @@ export class Consumer {
152
213
  this.fetchJson = fetchJson;
153
214
  this.now = now;
154
215
  this.deployment = { chainId: network.chain_id, settlement: network.settlement.toLowerCase() };
155
- this.cache = new Map();
156
- this.relayCache = null;
216
+ const shared = DISCOVERY.get(network) || new Map();
217
+ DISCOVERY.set(network, shared);
218
+ if (!shared.has(fetchJson)) {
219
+ shared.set(fetchJson, { relays: new Cached(() => this.loadRelays(), RELAY_CACHE_MS), providers: new Map(),
220
+ reputations: new Cached(() => this.loadReputations(), REPUTATION_CACHE_MS) });
221
+ }
222
+ this.discovery = shared.get(fetchJson);
157
223
  }
158
224
 
159
225
  /** On-chain reputation and verified probe verdicts for every Provider in view, cached. */
160
- async reputations() {
161
- if (this.reputationCache && Date.now() - this.reputationCache.at < REPUTATION_CACHE_MS) return this.reputationCache.value;
226
+ reputations({ waitMs = Infinity } = {}) {
227
+ const value = this.discovery.reputations.get();
228
+ if (!Number.isFinite(waitMs)) return value;
229
+ return Promise.race([value, new Promise((resolve) => setTimeout(resolve, waitMs, {}).unref())]);
230
+ }
231
+
232
+ async loadReputations() {
162
233
  const descriptors = new Map();
163
234
  const relaysByOwner = {};
164
- for (const relay of await this.relays()) {
235
+ await Promise.all((await this.relays()).map(async (relay) => {
165
236
  try {
166
237
  const owner = relay.owner || `0x${(await rpcCall(this.network.rpc_urls, "eth_call", [{ to: this.network.settlement,
167
238
  data: encodeCall("relaySignerOwner(address)", [["address", relay.signer]]) }, "latest"])).slice(26)}`;
168
239
  relaysByOwner[owner.toLowerCase()] = relay;
169
240
  } catch {}
170
241
  try { for (const descriptor of await this.providers(relay)) descriptors.set(descriptor.provider_signer, descriptor); } catch {}
171
- }
242
+ }));
172
243
  let value = {};
173
244
  try { value = await reputation(this, [...descriptors.values()], relaysByOwner); } catch {}
174
- this.reputationCache = { at: Date.now(), value };
175
245
  return value;
176
246
  }
177
247
 
248
+ /** Discover now what the first request would otherwise wait for; with `everyMs`, keep it fresh (serve). */
249
+ warm({ everyMs } = {}) {
250
+ const once = async () => {
251
+ try { await Promise.all((await this.relays()).map((relay) => this.providers(relay).catch(() => {}))); } catch {}
252
+ await this.reputations().catch(() => {});
253
+ };
254
+ const first = once();
255
+ if (everyMs) setInterval(once, everyMs).unref();
256
+ return first;
257
+ }
258
+
178
259
  /** Manifest Relays first, then directory Relays whose /health proves the announced signer. */
179
- async relays() {
180
- if (this.relayCache && Date.now() - this.relayCache.at < RELAY_CACHE_MS) return this.relayCache.relays;
260
+ relays() {
261
+ return this.discovery.relays.get();
262
+ }
263
+
264
+ async loadRelays() {
181
265
  const relays = this.network.relays.map((relay) => ({ ...relay }));
182
266
  const known = new Set(relays.map((relay) => relay.signer.toLowerCase()));
183
267
  try {
@@ -199,21 +283,30 @@ export class Consumer {
199
283
  } catch {}
200
284
  }
201
285
  } catch {}
202
- this.relayCache = { at: Date.now(), relays };
203
286
  return relays;
204
287
  }
205
288
 
289
+ /** A Relay's Providers whose signed descriptors verify now (a cached list is checked again on every use). */
206
290
  async providers(relay) {
207
- const cached = this.cache.get(relay.url);
208
- if (cached && Date.now() - cached.at < PROVIDER_CACHE_MS) return cached.providers;
209
- const { status, body } = await this.fetchJson(`${relay.url}/providers`, { ca: this.network.tls_ca, pin: relay.pin, timeoutMs: 10_000 });
210
- if (status !== 200) throw new Error(`Relay ${relay.url} /providers returned ${status}`);
291
+ let entry = this.discovery.providers.get(relay.url);
292
+ if (!entry) {
293
+ entry = new Cached(async () => {
294
+ const { status, body } = await this.fetchJson(`${relay.url}/providers`, { ca: this.network.tls_ca, pin: relay.pin, timeoutMs: 10_000 });
295
+ if (status !== 200) throw new Error(`Relay ${relay.url} /providers returned ${status}`);
296
+ return body.providers || [];
297
+ }, PROVIDER_CACHE_MS);
298
+ this.discovery.providers.set(relay.url, entry);
299
+ }
211
300
  const now = this.now();
212
- const providers = (body.providers || []).filter((descriptor) => {
213
- try { verifiedProvider(descriptor, this.deployment, now); return true; } catch { return false; }
301
+ return (await entry.get()).filter((descriptor) => {
302
+ // Signatures do not change: once verified, a descriptor stays good until its key or attestation expires.
303
+ const known = VERIFIED.get(descriptor);
304
+ if (known && known.from <= now && now < known.until) return true;
305
+ try { verifiedProvider(descriptor, this.deployment, now); } catch { return false; }
306
+ VERIFIED.set(descriptor, { from: now, until: Math.min(Number(descriptor.transport_key?.expires_at),
307
+ Number(descriptor.transport_attestation?.expires_at)) });
308
+ return true;
214
309
  });
215
- this.cache.set(relay.url, { at: Date.now(), providers });
216
- return providers;
217
310
  }
218
311
 
219
312
  /** The network's model catalog (manifest "models"): vendor, tier, capabilities. */
@@ -248,6 +341,7 @@ export class Consumer {
248
341
 
249
342
  async attempt({ endpoint, model, content, maxOutputTokens = 4096, options = {}, provider, onDelta }) {
250
343
  let lastError;
344
+ let raisedFee = 0;
251
345
  for (const relay of await this.relays()) {
252
346
  let candidates;
253
347
  try {
@@ -258,7 +352,7 @@ export class Consumer {
258
352
  // What the chain proves ranks first (recent fraud, a suspected downgrade, verified probe failures); then
259
353
  // Providers this Consumer is not already keeping busy; then reputation in coarse bands, shuffled within a
260
354
  // band so that many Consumers spread over the network instead of all queueing at its single best Provider.
261
- const records = await this.reputations().catch(() => ({}));
355
+ const records = await this.reputations({ waitMs: REPUTATION_WAIT_MS }).catch(() => ({}));
262
356
  const key = (descriptor) => {
263
357
  const [fraud, downgrade, failure, volume] = rankKey(descriptor, records[descriptor.provider_signer]);
264
358
  const busy = (this.inFlight.get(descriptor.provider_signer) || 0) >= Math.max(1, Number(descriptor.capacity) || 1) ? 1 : 0;
@@ -267,10 +361,15 @@ export class Consumer {
267
361
  const keyed = candidates.map((descriptor) => [key(descriptor), descriptor]);
268
362
  keyed.sort((a, b) => compareRank(a[0], b[0]));
269
363
  candidates = keyed.map(([, descriptor]) => descriptor);
270
- for (const descriptor of candidates) {
364
+ let retried = false;
365
+ for (let index = 0; index < candidates.length; index += 1) {
366
+ const descriptor = candidates[index];
367
+ // Authorize what this request can really cost, not the Consumer's whole per-request cap.
368
+ const plan = planRequest(descriptor, endpoint, content, maxOutputTokens, this.maxFee);
369
+ const maxFee = Math.min(Number(this.maxFee), Math.max(raisedFee || 0, plan.maxFee));
271
370
  const prepared = prepareRequest({
272
371
  descriptor, deployment: this.deployment, keyPrivate: this.keyPrivate, relaySigner: relay.signer,
273
- endpoint, model, content, maxOutputTokens, maxFee: this.maxFee, options, now: this.now(),
372
+ endpoint, model, content, maxOutputTokens: plan.maxOutputTokens, maxFee, options, now: this.now(),
274
373
  });
275
374
  let reply;
276
375
  const streamed = [];
@@ -312,6 +411,17 @@ export class Consumer {
312
411
  }
313
412
  lastError = Object.assign(new Error(reply.body?.error || `Relay returned ${reply.status}`),
314
413
  { status: reply.status, dispatched: reply.body?.dispatched });
414
+ // A Provider that found the max fee too low for this request says what it needs; ask again with that.
415
+ const needed = /worst-case price (\d+)/.exec(String(reply.body?.error || ""));
416
+ if (needed && reply.body?.dispatched === false) {
417
+ const required = Number(needed[1]);
418
+ if (required > Number(this.maxFee)) {
419
+ throw Object.assign(new Error(`this request may cost up to ${required}, above the per-request limit ${this.maxFee}`), { status: 402 });
420
+ }
421
+ raisedFee = Math.ceil(required * 1.2);
422
+ if (!retried) { retried = true; index -= 1; } // the same Provider, once, with the fee it asked for
423
+ continue;
424
+ }
315
425
  if (reply.body?.dispatched !== false || ![502, 503].includes(reply.status)) throw lastError;
316
426
  }
317
427
  }
@@ -362,6 +472,7 @@ async function dialectRequest(req, res, active, path, setWriter) {
362
472
 
363
473
  /** Local OpenAI-compatible endpoint (plus the Anthropic and Gemini dialects); nothing leaves this machine unsealed except pricing and routing. */
364
474
  export function serveConsumer(consumer, { host = "127.0.0.1", port = 8110, apiKey, dataDir, tenants = () => ({}) } = {}) {
475
+ consumer.warm({ everyMs: WARM_EVERY_MS }).catch(() => {}); // tenants share it
365
476
  const routes = dataDir ? consoleRoutes({ consumer, dataDir }) : {};
366
477
  // Every dialect's way of presenting a key: OpenAI Bearer, Anthropic x-api-key, Gemini x-goog-api-key or ?key=.
367
478
  const bearer = (req) => /^Bearer (.+)$/.exec(String(req.headers.authorization || ""))?.[1]
package/src/protocol.mjs CHANGED
@@ -111,3 +111,42 @@ export function openResponse(prepared, result, deployment, now = Math.floor(Date
111
111
  }
112
112
  return { response, receipt: signed, responsePlaintext: opened.payload };
113
113
  }
114
+
115
+ /**
116
+ * (input, output) tokens a request can cost at most, estimated exactly as the Provider does
117
+ * (mycomesh/protocol.py worst_case_tokens): content UTF-8 length / 3, plus the backend's own input, plus
118
+ * the full max_output_tokens.
119
+ */
120
+ export function worstCaseTokens(endpoint, content, maxOutputTokens, inputOverheadTokens = 0) {
121
+ const bytes = Buffer.byteLength(JSON.stringify(content ?? null), "utf8");
122
+ return [Math.ceil(bytes / 3) + Number(inputOverheadTokens || 0), Number(maxOutputTokens)];
123
+ }
124
+
125
+ /**
126
+ * The max fee to authorize for one request, and the output it may use: its worst-case price at the Provider's
127
+ * advertised network price with 50% headroom (a day's price step, estimation error), never above the
128
+ * Consumer's own cap. Relays reserve this amount while the request runs, so a realistic figure is what lets
129
+ * many requests share one deposit and one Provider's daily capacity at once. When even the worst case does
130
+ * not fit the cap, the output is trimmed to what the cap pays for, rather than having the Provider refuse.
131
+ */
132
+ export function planRequest(descriptor, endpoint, content, maxOutputTokens, cap) {
133
+ const prices = descriptor.prices || {};
134
+ const inPrice = Number(prices.input_per_1k || 0);
135
+ const outPrice = Number(prices.output_per_1k || 0);
136
+ const floor = Math.max(Number(prices.minimum_fee || 0), 1);
137
+ const [input] = worstCaseTokens(endpoint, content, 0, descriptor.input_overhead_tokens);
138
+ const worst = (output) => Math.max(Math.ceil((input * inPrice + output * outPrice) / 1000), floor);
139
+ let output = Number(maxOutputTokens);
140
+ if (worst(output) > Number(cap) && outPrice > 0) {
141
+ // What the cap pays for, keeping 10% for a price step between authorization and settlement.
142
+ output = Math.floor(((Number(cap) / 1.1) * 1000 - input * inPrice) / outPrice);
143
+ if (output < 16) {
144
+ throw Object.assign(new Error(`the per-request limit ${cap} cannot pay for this request at today's price`), { status: 402 });
145
+ }
146
+ }
147
+ return { maxFee: Math.min(Number(cap), Math.ceil(worst(output) * 1.5)), maxOutputTokens: output };
148
+ }
149
+
150
+ export function pricedMaxFee(descriptor, endpoint, content, maxOutputTokens, cap) {
151
+ return planRequest(descriptor, endpoint, content, maxOutputTokens, cap).maxFee;
152
+ }
@@ -48,11 +48,12 @@ async function ledgerEvents(network) {
48
48
  export async function reputation(consumer, descriptors, relaysByOwner) {
49
49
  const network = consumer.network;
50
50
  const records = {};
51
- for (const descriptor of descriptors) {
52
- try { records[descriptor.provider_signer] = await providerRecord(network, descriptor.provider_signer); } catch {}
53
- }
54
- let events = [];
55
- try { events = await ledgerEvents(network); } catch {}
51
+ const [, events] = await Promise.all([
52
+ Promise.all(descriptors.map(async (descriptor) => {
53
+ try { records[descriptor.provider_signer] = await providerRecord(network, descriptor.provider_signer); } catch {}
54
+ })),
55
+ ledgerEvents(network).catch(() => []),
56
+ ]);
56
57
  const byOwner = {};
57
58
  for (const record of Object.values(records)) {
58
59
  byOwner[record.owner] = { ...record, probes_passed: 0, probes_failed_verified: 0, capability_passed: 0, capability_failed_verified: 0 };