openvibers/openvibe.ai Commits

Community pools: route, verify and label a pool's offer as one machine's (#335)

OP OpenVibers committed 6 days ago · 57d60c2d55 · 3 files, +258 −14

* Community pools: route, verify and label a pool's offer as one machine's

An offer can carry pool { machines, head } (ai.offer-pool@1), which Network
adds from its approved pool record. AI reads it fail-closed (two to eight
distinct machine ids, the head is the offer's own machine, nothing else),
sends every request and canary to the head, and runs the same checks as for
one machine: a canary round when there is none recent, the model
fingerprint, spot re-runs. Verification is keyed on the exact membership,
so another member or head starts unverified.

A new pool, and one that failed verification, serves the free tier only and
earns nothing there; a single machine that fails is still not used. Answers,
the /sell view and the public offers carry a label, Community or Community
pool (N machines); the public pool lists its members as pseudonyms.

* Merge remote-tracking branch 'origin/main' into feature/pooled-machines

Changes

ddiff --git a/server/community/index.js b/server/community/index.js
iindex 7da6232..d918a1d 100644
---- a/server/community/index.js
++++ b/server/community/index.js
@@@ -12,8 +12,14 @@
* - spot re-runs: about 1% of answers are re-run on a trusted machine serving the same model and compared;
* - reputation: canaries, spot runs and observed uptime, 0-100.
* A new machine serves only the free tier (a run with no Balance payer, ctx.creditsRequired) until verified. The answer
-- * always carries `openvibe.community`, so a caller can see it came from a person's machine. The prompt is visible to
-- * that machine, which is why only opted-in requests ever go there.
++ * always carries `openvibe.community`, so a caller can see it came from a person's machine (its `label` is "Community", or
++ * "Community pool (N machines)" for a pool). The prompt is visible to that machine, which is why only opted-in requests
++ * ever go there.
++ *
++ * Pools (ai.offer-pool@1, server/community/pools.js): several of one owner's machines on one local network serving one
++ * model through llama.cpp RPC. Network names the pool on the head's offer; requests go to the head (which sees the
++ * prompt; the others see tensors), and the same checks as one machine run on the pool's answers, keyed on its exact
++ * membership. A new pool, and one that failed verification, serves the free tier only and earns nothing there.
*
* Money: a self-served run (the requester is the owner, or merged into it) earns 0. Verified credits are `pending`
* until the requester's own Balance charge exists in the same request (`spendable`), so none are spendable yet. A
@@@ -32,6 +38,7 @@
*/
const { createServiceTokenClient } = require('openvibe-sdk/auth');
const { AiError, sha256 } = require('../util');
++const pools = require('./pools');
const TEXT_OPS = new Set(['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages']);
const USR = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
@@@ -145,7 +152,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
const followersCache = new Map();
const aliasCache = new Map(); // subject -> survivor subject (account merges, 5 minutes)
-- const keyOf = (offer, model) => `${offer.offer_id}|${model}`;
++ const keyOf = (offer, model) => `${offer.offer_id}|${model}${pools.identity(offer.pool)}`;
/** One Network offer doc as this module uses it. Null for anything that is not a complete AI offer. */
function normalize(doc) {
@@@ -157,9 +164,12 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
const owner = doc.owner || d.owner;
if (typeof d.machine_id !== 'string' || typeof owner !== 'string' || !USR.test(owner)) return null;
if (!Array.isArray(d.models) || !d.models.length || !d.hours || !d.price) return null;
++ const pool = d.pool === undefined ? null : pools.poolOf(d.pool, d.machine_id);
++ if (d.pool !== undefined && !pool) return null;
const price = d.price;
return {
offer_id: String(doc.offer_id || d.offer_id), machine_id: d.machine_id, owner,
++ ...(pool ? { pool } : {}),
models: d.models.filter((m) => typeof m === 'string' && m).slice(0, 40),
hours: { start: String(d.hours.start || ''), end: String(d.hours.end || ''), tz: String(d.hours.tz || 'UTC') },
concurrency: Number(d.concurrency) || 1,
@@@ -431,7 +441,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
for (const model of offer.models) {
const key = keyOf(offer, model);
const s = await stateOf(key);
-- if (s && s.status === 'failed') continue;
++ if (s && s.status === 'failed' && !(offer.pool && freeTier(ctx))) continue;
if ((!s || s.status === 'new') && !freeTier(ctx)) continue;
if (ctx.communityModel && String(ctx.communityModel) !== model) continue;
if (!(await audienceAllows(offer, ctx))) continue;
@@@ -549,7 +559,16 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
async function execute({ route = null, operation = 'chat', req = {}, ctx = {} } = {}) {
const chosen = await choose({ ctx, operation, route });
if (!chosen) return null;
-- await ensure(chosen.offer, chosen.model);
++ try {
++ // The same checks as one machine: a canary round when there is no recent one, keyed on the pool's exact
++ // membership (pools.identity), so another member or head starts unverified.
++ await ensure(chosen.offer, chosen.model);
++ } catch (err) {
++ // A pool that failed verification is free tier only (the pooled machines design): it may still answer a
++ // free-tier request, earns nothing for it, and says `verification: failed`. A single machine that fails
++ // is not used at all, as before.
++ if (!(chosen.offer.pool && freeTier(ctx) && err && err.code === 'community.verification_failed')) throw err;
++ }
// The canary round may just have verified it: this first answer already earns and is no longer free-tier only.
const fresh = await stateOf(chosen.key);
if (fresh) { chosen.status = fresh.status; chosen.reputation = Number(fresh.reputation) || 0; }
@@@ -566,7 +585,7 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
const facts = {
resolved_model: `community/${chosen.model}`, revision: null, deployment: source,
provider_access: isEndpoint ? 'COMMUNITY_ENDPOINT' : 'COMMUNITY', rate_version: 'community', policy: 'community',
-- community: { kind: isEndpoint ? 'endpoint' : 'machine', machine_id: chosen.offer.machine_id, owner: chosen.offer.owner, model: chosen.model, price: chosen.offer.price,
++ community: { label: pools.label(chosen.offer.pool), ...(chosen.offer.pool ? { pool: chosen.offer.pool } : {}), kind: chosen.offer.pool ? 'pool' : isEndpoint ? 'endpoint' : 'machine', machine_id: chosen.offer.machine_id, owner: chosen.offer.owner, model: chosen.model, price: chosen.offer.price,
verification: chosen.status, reputation: chosen.reputation, free_tier: freeTier(ctx), self_served: selfServed },
};
if (typeof ctx.onRoute === 'function') {
@@@ -588,9 +607,11 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
throw err;
}
const usage = answer.usage || { input: 0, output: 0, cached: 0 };
++ // A spot re-run that disagrees drops this answer (the router falls back to the network) and fails the offer,
++ // for a pool as for one machine; a failed pool then serves the free tier only.
await spotCheck(chosen, messages, answer);
const latencyMs = clock.now() - t0;
-- const recorded = await recordUsage(chosen, { requestId, usage, verified: chosen.status !== 'new', requester, projectId: ctx.projectId || null, latencyMs,
++ const recorded = await recordUsage(chosen, { requestId, usage, verified: chosen.status === 'verified', requester, projectId: ctx.projectId || null, latencyMs,
chargeId: ctx.requesterChargeId || null });
const text = answer.text || '';
const message = answer.message || { role: 'assistant', content: text };
@@@ -604,9 +625,11 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
const explain = {
objective: 'community', reasons: [recorded.self_served
? 'self-served: the requester owns this offer, so the answer earns no credits'
-- : `${isEndpoint ? 'a community endpoint' : 'a community machine'} offered ${chosen.model}${chosen.offer.price.in_per_million || chosen.offer.price.out_per_million ? ` at ${chosen.offer.price.in_per_million}/${chosen.offer.price.out_per_million} credits per million` : ' for free'}`],
++ : `${isEndpoint ? 'a community endpoint' : chosen.offer.pool ? `a community pool of ${chosen.offer.pool.machines.length} machines` : 'a community machine'} offered ${chosen.model}${chosen.offer.price.in_per_million || chosen.offer.price.out_per_million ? ` at ${chosen.offer.price.in_per_million}/${chosen.offer.price.out_per_million} credits per million` : ' for free'}`],
selected: `${source}/${chosen.model}`, paid_by: 'community', candidates: [],
-- attempts: [{ key: 'community', provider: source, model: chosen.model, why: isEndpoint ? 'an opted-in request placed on a verified community endpoint' : 'an opted-in request placed on a verified community machine', status: 'ok', error: null }],
++ attempts: [{ key: 'community', provider: source, model: chosen.model, why: isEndpoint ? 'an opted-in request placed on a verified community endpoint'
++ : chosen.offer.pool ? `an opted-in request placed on a community pool through its head (${chosen.status === 'verified' ? 'verified' : 'free tier only'})`
++ : 'an opted-in request placed on a verified community machine', status: 'ok', error: null }],
community, openvibe: facts,
};
return {
@@@ -675,11 +698,11 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
const kudos = Math.max(0, Number(kudosRow.balance) || 0);
const offerViews = await Promise.all(list.map(async (o) => {
const openNow = o.hours ? withinHours(o, clock.now()) : null;
-- const out = { offer_id: o.offer_id, kind: o.kind || 'machine', machine_id: o.machine_id, models: o.models, hours: o.hours, concurrency: o.concurrency,
++ const out = { offer_id: o.offer_id, kind: o.pool ? 'pool' : o.kind || 'machine', label: pools.label(o.pool), ...(o.pool ? { pool: o.pool } : {}), machine_id: o.machine_id, models: o.models, hours: o.hours, concurrency: o.concurrency,
price: o.price, audience: o.audience, group: o.group, health: o.health, open_now: openNow, ready: o.ready && openNow !== false,
verification: null, reputation: 0, free_only: true, verified: false,
tokens_per_s: Math.max(0, ...served.filter((x) => x.offer_key.startsWith(`${o.offer_id}|`)).map((x) => x.tokens_per_s || 0)) || null };
-- const verified = o.models.map((m) => byKey.get(`${o.offer_id}|${m}`)).filter(Boolean);
++ const verified = o.models.map((m) => byKey.get(keyOf(o, m))).filter(Boolean);
if (verified.length) {
const s = verified.find((x) => x.status === 'verified') || verified[0];
const canaries = verified.reduce((n, x) => n + Number(x.canary_passes), 0);
@@@ -725,14 +748,19 @@ function createCommunity({ db, config, env = process.env, machines = null, endpo
/** What Exchange may show about community capacity: no owner subject, no machine id, no prompt. */
async function publicOffers() {
const list = await offers();
-- return list.map((o) => {
++ return Promise.all(list.map(async (o) => {
const openNow = withinHours(o, clock.now());
++ const states = o.pool ? await Promise.all(o.models.map((model) => stateOf(keyOf(o, model)))) : [];
return {
// The public id is a stable hash: Exchange can list and link an offer without publishing which machine it is.
-- offer_id: `community_${sha256(o.offer_id).slice(0, 16)}`, kind: o.kind || 'machine', models: o.models, hours: o.hours, concurrency: o.concurrency,
++ offer_id: `community_${sha256(o.offer_id).slice(0, 16)}`, kind: o.pool ? 'pool' : o.kind || 'machine', label: pools.label(o.pool), models: o.models,
++ // A pool's members as stable pseudonyms (Exchange shows the pool's card with its members), and whether
++ // every model on it is verified: a new or failed pool is free tier only.
++ ...(o.pool ? { pool: pools.publicPool(o.pool), free_only: !states.every((s) => s && s.status === 'verified'), verified: states.every((s) => s && s.status === 'verified') } : {}),
++ hours: o.hours, concurrency: o.concurrency,
price: o.price, audience: o.audience, health: o.health, open_now: openNow, ready: o.ready && openNow,
};
-- });
++ }));
}
return { enabled, offers, choose, execute, verify, sell, publicOffers, kudosBenefits, kudosBonus, queuePriority, _test: { normalize, withinHours, refreshReputation, fingerprintMatches, similar, messagesOf, freeTier } };
ddiff --git a/server/community/pools.js b/server/community/pools.js
nnew file mode 100644
iindex 0000000..f61b3c0
---- /dev/null
++++ b/server/community/pools.js
@@@ -0,0 +1,31 @@
++'use strict';
++/**
++ * The `pool` part of a community AI offer (ai.offer-pool@1; the pooled machines design, 2026-11-06): several of one
++ * owner's machines on one local network serving one model through llama.cpp RPC, answering through the head.
++ * Network adds it from its approved pool record; this module only reads it.
++ *
++ * poolOf(value, machineId) { machines, head } sorted, or null when it is not exactly a valid pool whose head is the
++ * offer's own machine (a malformed pool drops the whole offer: fail closed)
++ * label(pool) "Community pool (N machines)", or "Community" for one machine
++ * identity(pool) the suffix of the verification key: another member or head is a new, unverified pool
++ * publicPool(pool) the members as stable pseudonyms, for Exchange (never a machine id)
++ */
++const { sha256 } = require('../util');
++
++const DEVICE = /^dev_[0-9A-HJKMNP-TV-Z]{26}$/;
++
++function poolOf(value, machine) {
++ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some((k) => !['machines', 'head'].includes(k))) return null;
++ const members = value.machines;
++ if (!Array.isArray(members) || members.length < 2 || members.length > 8 || new Set(members).size !== members.length
++ || members.some((m) => typeof m !== 'string' || !DEVICE.test(m)) || !members.includes(value.head) || value.head !== machine) return null;
++ return { machines: [...members].sort(), head: value.head };
++}
++const label = (pool) => (pool ? `Community pool (${pool.machines.length} machines)` : 'Community');
++const identity = (pool) => (pool ? `|pool:${sha256(JSON.stringify(pool)).slice(0, 24)}` : '');
++function publicPool(pool) {
++ const pseudonym = (id) => `machine_${sha256(id).slice(0, 16)}`;
++ return { machines: pool.machines.map(pseudonym), head: pseudonym(pool.head) };
++}
++
++module.exports = { poolOf, label, identity, publicPool };
ddiff --git a/test/community-pools.test.js b/test/community-pools.test.js
nnew file mode 100644
iindex 0000000..8b9ed59
---- /dev/null
++++ b/test/community-pools.test.js
@@@ -0,0 +1,185 @@
++'use strict';
++// Pooled community offers (ai.offer-pool@1): a pool's offer routes through its head, is verified as one machine is,
++// is labelled "Community pool (N machines)", and a pool that is new or failed verification serves the free tier only.
++const assert = require('node:assert/strict');
++const { boot, suite } = require('./helpers');
++const { createCommunity } = require('../server/community');
++const pools = require('../server/community/pools');
++
++const t = suite('community pools');
++let harness;
++async function fresh() {
++ if (!harness) harness = await boot({ env: { AI_COMMUNITY: 'false' } });
++ await harness.db.prepare('DELETE FROM community_usage').run();
++ await harness.db.prepare('DELETE FROM community_offers').run();
++ return harness;
++}
++const owner = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A1';
++const peerOwner = 'usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A2';
++const head = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D1';
++const member = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D2';
++const third = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D3';
++const single = 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2D9';
++const FREE = { communityOptIn: true, communityFree: true };
++const PAID = { communityOptIn: true };
++const ask = { messages: [{ role: 'user', content: 'hello' }] };
++
++const offerDoc = (machine, extra = {}) => ({ offer_id: `ai:${machine}`, machine_id: machine, owner, models: ['pool-model'],
++ hours: { start: '00:00', end: '00:00', tz: 'UTC' }, concurrency: 1,
++ price: { in_per_million: 1, out_per_million: 2 }, audience: 'everyone', group: null,
++ ready: true, health: { status: 'up' }, ...extra });
++const poolDoc = (machines = [head, member]) => offerDoc(head, { pool: { machines, head } });
++
++/**
++ * A community module over fake Network offers and fake machines. `answers[machine]` is how a machine answers:
++ * 'good' passes every canary, 'bad' fails the arithmetic canary, 'odd' passes canaries but answers prompts differently.
++ */
++function setup(db, docs, answers = {}, { spotRate = 0 } = {}) {
++ const calls = [];
++ const community = createCommunity({ db, config: { networkInternalUrl: 'http://network.test', community: { enabled: true, refreshMs: 1, spotRate } },
++ env: {}, tokens: { getToken: async () => 'test-token' },
++ fetchImpl: async () => ({ ok: true, json: async () => ({ offers: docs }) }),
++ machines: { enabled: true, chat: async (request) => {
++ calls.push(request);
++ const how = answers[request.machine] || 'good';
++ const message = request.messages.at(-1).content;
++ const text = message.includes('OPENVIBE-CANARY') ? 'OPENVIBE-CANARY-7F3A'
++ : message.includes('17 + 25') ? (how === 'bad' ? '41' : '42')
++ : message.includes('canary-ok') ? 'canary-ok'
++ : how === 'odd' ? 'purple elephants dance quietly tonight' : 'Hello from the answering machine';
++ return { text, model: request.model, usage: { input: 10, output: 5 } };
++ } }, log: { warn() {} }, random: () => 0 });
++ return { community, calls };
++}
++const usage = async (db) => db.prepare('SELECT credits_micro, verified, earning_status FROM community_usage ORDER BY created_at').all();
++
++t.test('a pool offer routes through its head, verifies like one machine, and is labelled', async () => {
++ const h = await fresh();
++ const { community: c, calls } = setup(h.db, [poolDoc()]);
++ const [offer] = await c.offers();
++ assert.deepEqual(offer.pool, { machines: [head, member].sort(), head });
++ assert.equal(await c.choose({ ctx: PAID, operation: 'chat' }), null, 'a new pool cannot receive paid work');
++ const out = await c.execute({ ctx: { ...FREE, runId: 'pool-1' }, req: ask });
++ assert.equal(out.explain.community.label, 'Community pool (2 machines)');
++ assert.equal(out.explain.community.kind, 'pool');
++ assert.deepEqual(out.explain.community.pool, offer.pool);
++ assert.equal(out.explain.community.verification, 'verified', 'the canary round ran on the pool');
++ assert.equal(calls.length, 4, 'three canaries and the prompt');
++ assert.ok(calls.every((r) => r.machine === head && r.community === true), 'canaries and prompts all go to the head');
++ assert.ok(calls.every((r) => r.machine !== member), 'a member is never addressed directly');
++ // Verified once, like one machine: the next request is just the prompt.
++ await c.execute({ ctx: { ...PAID, runId: 'pool-2' }, req: ask });
++ assert.equal(calls.length, 5, 'no canary round on every request');
++ assert.ok(await c.choose({ ctx: PAID, operation: 'chat' }), 'a verified pool may serve paid work');
++ const rows = await usage(h.db);
++ assert.equal(rows.length, 2);
++ assert.equal(rows[1].verified, true);
++ assert.ok(Number(rows[1].credits_micro) > 0, 'a verified pool earns');
++
++ const pub = (await c.publicOffers())[0];
++ assert.equal(pub.kind, 'pool');
++ assert.equal(pub.label, 'Community pool (2 machines)');
++ assert.equal(pub.free_only, false);
++ assert.equal(pub.verified, true);
++ assert.equal(pub.pool.machines.length, 2);
++ assert.ok(pub.pool.machines.includes(pub.pool.head));
++ const shown = JSON.stringify(pub);
++ assert.ok(!shown.includes(owner) && !shown.includes(head) && !shown.includes(member), 'no owner or machine id is public');
++
++ const sold = await c.sell(owner);
++ assert.equal(sold.offers[0].kind, 'pool');
++ assert.equal(sold.offers[0].label, 'Community pool (2 machines)');
++ assert.equal(sold.offers[0].verified, true);
++});
++
++t.test('a pool that fails verification serves the free tier only, earns nothing, and is labelled failed', async () => {
++ const h = await fresh();
++ const { community: c, calls } = setup(h.db, [poolDoc()], { [head]: 'bad' });
++ const first = await c.execute({ ctx: { ...FREE, runId: 'failed-1' }, req: ask });
++ assert.equal(first.explain.community.verification, 'failed');
++ assert.equal(first.explain.community.label, 'Community pool (2 machines)');
++ assert.match(first.explain.attempts[0].why, /free tier only/);
++ assert.equal(await c.choose({ ctx: PAID, operation: 'chat' }), null, 'a failed pool never gets paid work');
++ assert.ok(await c.choose({ ctx: FREE, operation: 'chat' }), 'a failed pool stays on the free tier');
++ const before = calls.length;
++ const second = await c.execute({ ctx: { ...FREE, runId: 'failed-2' }, req: ask });
++ assert.equal(second.explain.community.verification, 'failed');
++ assert.equal(calls.length, before + 1, 'a failed pool is not re-verified by traffic');
++ const rows = await usage(h.db);
++ assert.equal(rows.length, 2);
++ assert.ok(rows.every((r) => Number(r.credits_micro) === 0 && r.verified === false), 'nothing earned on a failed pool');
++ const pub = (await c.publicOffers())[0];
++ assert.equal(pub.free_only, true);
++ assert.equal(pub.verified, false);
++});
++
++t.test('a single machine that fails is still not used, even for the free tier', async () => {
++ const h = await fresh();
++ const { community: c } = setup(h.db, [offerDoc(single)], { [single]: 'bad' });
++ await assert.rejects(c.execute({ ctx: { ...FREE, runId: 'single-1' }, req: ask }), (err) => err.code === 'community.verification_failed');
++ assert.equal(await c.choose({ ctx: FREE, operation: 'chat' }), null);
++ assert.equal((await c.publicOffers())[0].label, 'Community');
++});
++
++t.test('a spot re-run that disagrees drops the answer and leaves the pool free tier only', async () => {
++ const h = await fresh();
++ // A dearer trusted machine of another owner: the cheaper pool is chosen first, the peer re-runs the spot.
++ const peer = { ...offerDoc(single), owner: peerOwner, price: { in_per_million: 5, out_per_million: 5 } };
++ const { community: c } = setup(h.db, [poolDoc(), peer], { [head]: 'odd' }, { spotRate: 1 });
++ const peerOffer = (await c.offers()).find((o) => !o.pool);
++ await c.verify(peerOffer, 'pool-model');
++ // A paid request on the verified pool: the canaries pass, then the spot re-run on the trusted peer disagrees.
++ const poolOffer = (await c.offers()).find((o) => o.pool);
++ await c.verify(poolOffer, 'pool-model');
++ await assert.rejects(c.execute({ ctx: { ...PAID, communityModel: 'pool-model', runId: 'spot-1' }, req: ask }),
++ (err) => err.code === 'community.verification_failed', 'the disagreeing answer is not returned');
++ const left = await c.choose({ ctx: PAID, operation: 'chat' });
++ assert.ok(left && !left.offer.pool, 'the pool lost paid work; the trusted machine keeps it');
++ const pub = (await c.publicOffers()).find((o) => o.kind === 'pool');
++ assert.equal(pub.free_only, true);
++ // Without the peer, the failed pool still answers the free tier, and only the free tier.
++ const alone = setup(h.db, [poolDoc()], { [head]: 'odd' });
++ assert.equal(await alone.community.choose({ ctx: PAID, operation: 'chat' }), null);
++ const freeOut = await alone.community.execute({ ctx: { ...FREE, runId: 'spot-2' }, req: ask });
++ assert.equal(freeOut.explain.community.verification, 'failed');
++ assert.equal(freeOut.explain.community.credits_micro, 0);
++});
++
++t.test('another member or head is a new, unverified pool', async () => {
++ const h = await fresh();
++ const a = setup(h.db, [poolDoc()]);
++ await a.community.execute({ ctx: { ...FREE, runId: 'm-1' }, req: ask });
++ assert.ok(await a.community.choose({ ctx: PAID, operation: 'chat' }), 'verified as [head, member]');
++ const b = setup(h.db, [poolDoc([head, member, third])]);
++ assert.equal(await b.community.choose({ ctx: PAID, operation: 'chat' }), null, 'a third member makes it unverified again');
++ const out = await b.community.execute({ ctx: { ...FREE, runId: 'm-2' }, req: ask });
++ assert.equal(out.explain.community.label, 'Community pool (3 machines)');
++ assert.equal(b.calls.length, 4, 'the new membership runs its own canary round');
++});
++
++t.test('a malformed pool fails closed', async () => {
++ const valid = pools.poolOf({ machines: [member, head], head }, head);
++ assert.deepEqual(valid, { machines: [head, member].sort(), head });
++ const bad = [
++ { machines: [head, head], head },
++ { machines: [head, member], head: third },
++ { machines: [head], head },
++ { machines: [head, member], head, address: '192.168.1.2/24' },
++ { machines: [head, member], head, certificates: ['-----BEGIN CERTIFICATE-----'] },
++ { machines: [head, 'node:abc'], head },
++ { machines: Array.from({ length: 9 }, (_, i) => `dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2E${i}`), head: 'dev_01J8Z3Q4R5S6T7V8W9X0Y1Z2E0' },
++ [head, member],
++ 'pool',
++ ];
++ for (const value of bad) assert.equal(pools.poolOf(value, head), null, JSON.stringify(value));
++ assert.equal(pools.poolOf({ machines: [head, member], head: member }, head), null, 'the head is the offer\'s own machine');
++ assert.notEqual(pools.identity(valid), pools.identity({ machines: [head, third].sort(), head }));
++ assert.equal(pools.identity(null), '', 'one machine keeps its verification key');
++ assert.equal(pools.label(null), 'Community');
++ const h = await fresh();
++ const doc = poolDoc(); doc.pool.head = third;
++ assert.deepEqual(await setup(h.db, [doc]).community.offers(), [], 'an offer with a malformed pool is dropped whole');
++});
++
++t.test('close the isolated test server', async () => { if (harness) await harness.stop(); });
++t.run();