openzoo 0.51.17 → 0.51.19
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/README.md +8 -0
- package/launcher/savings.cjs +4 -0
- package/lib/config.js +2 -1
- package/lib/demo.js +2 -1
- package/lib/info.js +3 -2
- package/lib/namespace.js +20 -17
- package/lib/pay.js +2 -2
- package/lib/proxy.js +6 -20
- package/lib/sonar.js +6 -9
- package/lib/voice.js +11 -13
- package/lib/xbot.js +10 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,6 +230,14 @@ Pin the key with `OPENZOO_TUNNEL_TOKEN` if your IDE stores it. Keys never leave
|
|
|
230
230
|
|
|
231
231
|
**MCP clients don't need any of this.** If your tool speaks MCP, use the hosted server at `https://mcp.openzoo.fun/mcp` — cloud-reachable, no local process, mint a wallet with `zoo_wallet`.
|
|
232
232
|
|
|
233
|
+
## Memory authentication
|
|
234
|
+
|
|
235
|
+
Every memory request uses a wallet-signed namespace. Signing errors stop the request; unsigned requests and the shared default bucket are rejected by the gateway. Voice and Sonar now use that same gateway, including scored recall. Re-ingest contexts previously stored in a local daemon or the old shared bucket.
|
|
236
|
+
|
|
237
|
+
The gateway signs each storage request over its exact payload, route, timestamp, tenant and context. The backing store verifies that proof and retains signed receipts with saved contexts or the tenant's memory notes. Signing authenticates access and integrity; it does not encrypt uploaded text. Never include private keys in an uploaded corpus.
|
|
238
|
+
|
|
239
|
+
Public proxy tunnels require the configured token for memory and inference. Arbitrary first-use tokens and anonymous binds are no longer accepted. Quote discovery uses an unfunded signing identity so it cannot consume the operator's credit.
|
|
240
|
+
|
|
233
241
|
## The wallet model
|
|
234
242
|
|
|
235
243
|
- **Burner, local, yours.** A keypair in `~/.openzoo/wallet.json`, created on first run, chmod 600. Keys never leave your machine — the zoo only ever sees signed transfers.
|
package/launcher/savings.cjs
CHANGED
|
@@ -89,6 +89,10 @@ function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
|
|
|
89
89
|
const d = await r.json();
|
|
90
90
|
if (!r.ok || d.youAreTalkingTo !== 'openzoo proxy') throw Error('Unavailable');
|
|
91
91
|
last = d;
|
|
92
|
+
if (wallet && d.solana && typeof d.evm === 'string' && (wallet.solana !== d.solana || wallet.evm !== d.evm)) {
|
|
93
|
+
wallet = { solana: d.solana, evm: d.evm };
|
|
94
|
+
if (!window.isDestroyed()) window.webContents.send('deposit-wallet', wallet);
|
|
95
|
+
}
|
|
92
96
|
if (!window.isDestroyed()) window.webContents.send('savings', { ...d, online: true });
|
|
93
97
|
} catch { if (!window.isDestroyed()) window.webContents.send('savings', { ...last, online: false }); }
|
|
94
98
|
finally { busy = false; }
|
package/lib/config.js
CHANGED
|
@@ -207,9 +207,10 @@ export function unfundableRails(liveRailNames) {
|
|
|
207
207
|
* currently offering are reported separately so nobody funds a dead lane.
|
|
208
208
|
*/
|
|
209
209
|
export async function liveRails() {
|
|
210
|
+
const { withQuoteNamespace } = await import('./namespace.js');
|
|
210
211
|
const r = await fetchHeaders(`${config.apiBase}/v1/chat/completions`, {
|
|
211
212
|
method: 'POST',
|
|
212
|
-
headers: { 'content-type': 'application/json' },
|
|
213
|
+
headers: withQuoteNamespace({ 'content-type': 'application/json' }),
|
|
213
214
|
body: JSON.stringify({ model: 'nvidia/nemotron-3.5-lightning', messages: [{ role: 'user', content: 'ping' }], max_tokens: 1 }),
|
|
214
215
|
});
|
|
215
216
|
if (r.status !== 402) { await r.text().catch(() => {}); return null; }
|
package/lib/demo.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withNamespace } from './namespace.js';
|
|
1
2
|
import { config, fundingLine } from './config.js';
|
|
2
3
|
import { PayClient, UnderfundedError } from './pay.js';
|
|
3
4
|
import { parse402, pickAccept, tokenBalance } from './x402.js';
|
|
@@ -38,7 +39,7 @@ async function quoteFor(bodyObj, headers = {}) {
|
|
|
38
39
|
const spin = startSpinner(big ? `uploading ${mb}MB body` : 'asking the zoo for a live quote');
|
|
39
40
|
try {
|
|
40
41
|
const r = await postWithUploadSignal(`${config.apiBase}/v1/chat/completions`, body, {
|
|
41
|
-
headers,
|
|
42
|
+
headers: withNamespace(headers),
|
|
42
43
|
onUploaded: () => { if (big) spin.update(`zoo pricing ~${approxTokens}k tokens`); },
|
|
43
44
|
});
|
|
44
45
|
if (r.status !== 402) throw new Error(`expected 402 quote, got HTTP ${r.status}: ${(await r.text()).slice(0, 300)}`);
|
package/lib/info.js
CHANGED
|
@@ -62,10 +62,11 @@ export async function quotedPrices() {
|
|
|
62
62
|
// that number.
|
|
63
63
|
//
|
|
64
64
|
// The chat challenge prices each asset at its real spot (DexScreener), and
|
|
65
|
-
//
|
|
65
|
+
// uses an unfunded signing identity so discovery cannot spend wallet credit.
|
|
66
|
+
const { withQuoteNamespace } = await import('./namespace.js');
|
|
66
67
|
const r = await fetchHeaders(`${config.apiBase}/v1/chat/completions`, {
|
|
67
68
|
method: 'POST',
|
|
68
|
-
headers: { 'content-type': 'application/json' },
|
|
69
|
+
headers: withQuoteNamespace({ 'content-type': 'application/json' }),
|
|
69
70
|
body: JSON.stringify({ model: config.defaultModel || 'anthropic/claude-sonnet-5', max_tokens: 1, messages: [{ role: 'user', content: 'x' }] }),
|
|
70
71
|
});
|
|
71
72
|
if (r.status !== 402) return out;
|
package/lib/namespace.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import { Keypair } from '@solana/web3.js';
|
|
2
3
|
import { loadOrCreateWallet } from './wallet.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -59,22 +60,16 @@ import { loadOrCreateWallet } from './wallet.js';
|
|
|
59
60
|
* requests across time. A value every caller sends identically discloses
|
|
60
61
|
* nothing at all.
|
|
61
62
|
*
|
|
62
|
-
* BREAKING:
|
|
63
|
-
*
|
|
64
|
-
* reaches pre-existing context ids by id, but anything relying on the old
|
|
65
|
-
* namespace should be re-bound.
|
|
63
|
+
* BREAKING: old namespaces stay isolated. Re-bind their corpora under the
|
|
64
|
+
* authenticated namespace; there is no shared-memory fallback.
|
|
66
65
|
*/
|
|
67
66
|
export const STACC_NAMESPACE = 'stacc';
|
|
68
67
|
|
|
69
68
|
export function namespaceHeaderValue(wallet) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return STACC_NAMESPACE;
|
|
75
|
-
} catch {
|
|
76
|
-
return ''; // no wallet (read-only use): fall back to the shared tenant
|
|
77
|
-
}
|
|
69
|
+
// A failed signer must fail the operation; never send an unsigned request.
|
|
70
|
+
if (wallet !== undefined && !wallet?.keypair) throw new Error('The supplied wallet cannot sign.');
|
|
71
|
+
if (!wallet?.keypair) loadOrCreateWallet();
|
|
72
|
+
return STACC_NAMESPACE;
|
|
78
73
|
}
|
|
79
74
|
|
|
80
75
|
/**
|
|
@@ -121,11 +116,14 @@ function signNamespace(namespace, wallet) {
|
|
|
121
116
|
/** Merge the namespace header — and its proof — into any headers object. */
|
|
122
117
|
export function withNamespace(headers = {}, wallet) {
|
|
123
118
|
const ns = namespaceHeaderValue(wallet);
|
|
124
|
-
if (!ns)
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
if (!ns) throw new Error('A wallet-signed namespace is required; shared memory is disabled.');
|
|
120
|
+
// The gateway rejects an unsigned namespace. Signing failure must stop
|
|
121
|
+
// the operation locally instead of sending an unauthenticated request.
|
|
122
|
+
const clean = Object.fromEntries(new Headers(headers).entries());
|
|
123
|
+
for (const name of Object.keys(clean)) {
|
|
124
|
+
if (name === 'x-openzoo-session' || name.startsWith('x-openzoo-namespace')) delete clean[name];
|
|
125
|
+
}
|
|
126
|
+
return { ...clean, 'x-openzoo-namespace': ns, ...signNamespace(ns, wallet) };
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
// Bitcoin-alphabet base58, matching what the gateway's bs58 decode expects.
|
|
@@ -147,3 +145,8 @@ function base58encode(bytes) {
|
|
|
147
145
|
for (let i = digits.length - 1; i >= 0; i--) out += B58[digits[i]];
|
|
148
146
|
return out;
|
|
149
147
|
}
|
|
148
|
+
|
|
149
|
+
/** Price discovery must never consume the operator's existing gateway credit. */
|
|
150
|
+
export function withQuoteNamespace(headers = {}) {
|
|
151
|
+
return withNamespace(headers, { keypair: Keypair.generate() });
|
|
152
|
+
}
|
package/lib/pay.js
CHANGED
|
@@ -426,7 +426,7 @@ export class PayClient {
|
|
|
426
426
|
onStage?.('paying');
|
|
427
427
|
let response = await fetchHeaders(url, {
|
|
428
428
|
...init,
|
|
429
|
-
headers: { ...stripAuthorization(init.headers || {}), ...paymentHeaders(payment.header) },
|
|
429
|
+
headers: withNamespace({ ...stripAuthorization(init.headers || {}), ...paymentHeaders(payment.header) }, { keypair: this.keypair }),
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
// A REFUSED ATOMIC ROW MUST NOT TAKE THE WHOLE CALL DOWN.
|
|
@@ -450,7 +450,7 @@ export class PayClient {
|
|
|
450
450
|
const retry = await this.buildPaymentFor(cand, onStage, quote);
|
|
451
451
|
const again = await fetchHeaders(url, {
|
|
452
452
|
...init,
|
|
453
|
-
headers: { ...stripAuthorization(init.headers || {}), ...paymentHeaders(retry.header) },
|
|
453
|
+
headers: withNamespace({ ...stripAuthorization(init.headers || {}), ...paymentHeaders(retry.header) }, { keypair: this.keypair }),
|
|
454
454
|
});
|
|
455
455
|
if (again.status !== 402) {
|
|
456
456
|
accept = cand; payment = retry; response = again;
|
package/lib/proxy.js
CHANGED
|
@@ -640,7 +640,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
640
640
|
// Requests that arrived over the public quick-tunnel URL carry cloudflared's
|
|
641
641
|
// headers; nothing dialing 127.0.0.1 directly does. That distinction is what
|
|
642
642
|
// lets localhost stay keyless while the SAME port is safely public.
|
|
643
|
-
const viaTunnel = !requireToken
|
|
643
|
+
const viaTunnel = !requireToken
|
|
644
644
|
&& Boolean(req.headers['cf-connecting-ip'] || req.headers['cf-ray']);
|
|
645
645
|
// Auth first: refuse before reading a body, forwarding, quoting or paying.
|
|
646
646
|
if (requireToken) {
|
|
@@ -676,6 +676,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
676
676
|
youAreTalkingTo: 'openzoo proxy',
|
|
677
677
|
version: ozVersion,
|
|
678
678
|
solana: client.address,
|
|
679
|
+
evm: client.evmAddress,
|
|
679
680
|
yourEndpoint: self,
|
|
680
681
|
reachedVia: viaTunnel ? 'public tunnel' : 'localhost',
|
|
681
682
|
publicTunnel: tunnelGate?.publicUrl ? `${tunnelGate.publicUrl}/v1` : null,
|
|
@@ -708,7 +709,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
708
709
|
upstream: config.apiBase,
|
|
709
710
|
payment: 'x402 per request from the operator\'s local burner wallet - no API key, no account',
|
|
710
711
|
auth: viaTunnel
|
|
711
|
-
? 'this public URL requires the oz_… bearer for
|
|
712
|
+
? 'this public URL requires the configured oz_… bearer for memory and inference; /v1/models is public'
|
|
712
713
|
: 'localhost is keyless',
|
|
713
714
|
shim: 'pure passthrough — request bodies are forwarded byte-for-byte; all message/model handling is on the backend',
|
|
714
715
|
tools: ['zoo_bind', 'zoo_ask', 'zoo_status', 'zoo_models', 'zoo_wallet', 'zoo_contexts'],
|
|
@@ -720,21 +721,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
720
721
|
|
|
721
722
|
if (viaTunnel) {
|
|
722
723
|
const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
// FIRST other key to present itself claims the tunnel for the session.
|
|
726
|
-
// OPENZOO_TUNNEL_STRICT=1 restores exact-match only.
|
|
727
|
-
const strict = process.env.OPENZOO_TUNNEL_STRICT === '1';
|
|
728
|
-
let authed = got === tunnelGate.token;
|
|
729
|
-
if (!authed && !strict && got.length >= 8) {
|
|
730
|
-
if (!tunnelGate.adopted) {
|
|
731
|
-
tunnelGate.adopted = got;
|
|
732
|
-
log(`public url: adopted the caller's key (first-use) — later keys must match it`);
|
|
733
|
-
authed = true;
|
|
734
|
-
} else {
|
|
735
|
-
authed = got === tunnelGate.adopted;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
724
|
+
// Only the configured token authorizes signing with the operator's wallet.
|
|
725
|
+
const authed = Boolean(tunnelGate?.token) && got === tunnelGate.token;
|
|
738
726
|
const p = (req.url || '').split('?')[0];
|
|
739
727
|
// DISCOVERY IS FREE. Reads that cost nothing and leak nothing go through
|
|
740
728
|
// without the key; money paths stay gated.
|
|
@@ -758,9 +746,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
758
746
|
// paths below stay gated; the worst a stranger can do here is spend
|
|
759
747
|
// the sidecar's disk.
|
|
760
748
|
const freeRead = req.method === 'GET' && (p === '/v1/models' || p.startsWith('/v1/models/'));
|
|
761
|
-
|
|
762
|
-
if (freeBind) log(`public url: unauthenticated bind allowed (free endpoint) from ${req.socket.remoteAddress}`);
|
|
763
|
-
if (!freeRead && !freeBind) {
|
|
749
|
+
if (!freeRead) {
|
|
764
750
|
log(`public url: 401 ${req.method} ${req.url}`);
|
|
765
751
|
jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at the operator\'s proxy startup — GET / for discovery, GET /v1/models is open');
|
|
766
752
|
return;
|
package/lib/sonar.js
CHANGED
|
@@ -38,6 +38,7 @@ import crypto from 'node:crypto';
|
|
|
38
38
|
import { PublicKey } from '@solana/web3.js';
|
|
39
39
|
import { loadDotenv } from './dotenv.js';
|
|
40
40
|
import { config } from './config.js';
|
|
41
|
+
import { withNamespace } from './namespace.js';
|
|
41
42
|
|
|
42
43
|
// Before config.js is consulted, so OPENZOO_RPC from .env wins over the
|
|
43
44
|
// public default without anyone pasting a credentialed URL onto a
|
|
@@ -1588,9 +1589,6 @@ export function vocabularyFor(combo) {
|
|
|
1588
1589
|
* Every recalled name is still verified by hashing, so a bad recall costs
|
|
1589
1590
|
* a few microseconds and never a wrong answer.
|
|
1590
1591
|
*/
|
|
1591
|
-
const LECORE = process.env.OPENZOO_LECORE_URL || 'http://127.0.0.1:8787';
|
|
1592
|
-
const LECORE_TOKEN = process.env.OPENZOO_LECORE_TOKEN || 'hrr-lab-token';
|
|
1593
|
-
const LECORE_TENANT = process.env.OPENZOO_LECORE_TENANT || 'claude-code';
|
|
1594
1592
|
|
|
1595
1593
|
export async function bindCorpus({ log = () => {}, sample = 0 } = {}) {
|
|
1596
1594
|
let comps = { archetypes: [] };
|
|
@@ -1616,11 +1614,10 @@ export async function bindCorpus({ log = () => {}, sample = 0 } = {}) {
|
|
|
1616
1614
|
let contextId = null;
|
|
1617
1615
|
const BATCH = 500;
|
|
1618
1616
|
for (let i = 0; i < items.length; i += BATCH) {
|
|
1619
|
-
const res = await fetch(`${
|
|
1617
|
+
const res = await fetch(`${config.apiBase}/v1/hrr/bind`, {
|
|
1620
1618
|
method: 'POST',
|
|
1621
|
-
headers: { 'content-type': 'application/json'
|
|
1619
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
1622
1620
|
body: JSON.stringify({
|
|
1623
|
-
tenant_id: LECORE_TENANT,
|
|
1624
1621
|
items: items.slice(i, i + BATCH),
|
|
1625
1622
|
...(contextId ? { context_id: contextId } : {}),
|
|
1626
1623
|
}),
|
|
@@ -1643,10 +1640,10 @@ export async function recallVocabulary(query, { topK = 24 } = {}) {
|
|
|
1643
1640
|
const ctx = lecoreContext();
|
|
1644
1641
|
if (!ctx) return [];
|
|
1645
1642
|
try {
|
|
1646
|
-
const res = await fetch(`${
|
|
1643
|
+
const res = await fetch(`${config.apiBase}/v1/hrr/recall`, {
|
|
1647
1644
|
method: 'POST',
|
|
1648
|
-
headers: { 'content-type': 'application/json'
|
|
1649
|
-
body: JSON.stringify({
|
|
1645
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
1646
|
+
body: JSON.stringify({ context_id: ctx, query, top_k: topK }),
|
|
1650
1647
|
});
|
|
1651
1648
|
if (!res.ok) return [];
|
|
1652
1649
|
const items = (await res.json()).items || [];
|
package/lib/voice.js
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
* TIERED CONTEXTS, CASCADED RETRIEVAL (leCore's retrieval_dispatch shape):
|
|
14
14
|
* the corpus binds into progressively larger tiers — cream (best few
|
|
15
15
|
* thousand turns), telegram (all of it), twitter (all of it) — and every
|
|
16
|
-
* bind
|
|
17
|
-
* the local leCore daemon (scored recall). A rewrite recalls against
|
|
16
|
+
* bind uses the authenticated gateway for both text and scored item recall. A rewrite recalls against
|
|
18
17
|
* cream first; when the top-1/top-2 margin says the match is ambiguous,
|
|
19
18
|
* it ESCALATES into the full tiers and fuses. Exemplars go inline; the
|
|
20
19
|
* gateway context rides as x-hrr-context only when the daemon is down.
|
|
@@ -29,6 +28,7 @@ import os from 'node:os';
|
|
|
29
28
|
import path from 'node:path';
|
|
30
29
|
import { splitIntoParts } from './bindpath.js';
|
|
31
30
|
import { config } from './config.js';
|
|
31
|
+
import { withNamespace } from './namespace.js';
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* VOICE CALLS ARE SLOW CALLS, AND THE DEFAULT TIMEOUT ASSUMES THEY ARE NOT.
|
|
@@ -44,9 +44,6 @@ if (!process.env.OPENZOO_UPSTREAM_HEADERS_MS) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const GATEWAY = config.apiBase;
|
|
47
|
-
const DAEMON = process.env.OPENZOO_LECORE_URL || 'http://127.0.0.1:8787';
|
|
48
|
-
const DAEMON_TOKEN = process.env.OPENZOO_LECORE_TOKEN || 'hrr-lab-token';
|
|
49
|
-
const DAEMON_TENANT = process.env.OPENZOO_LECORE_TENANT || 'claude-code';
|
|
50
47
|
|
|
51
48
|
const STATE_FILE = process.env.OPENZOO_VOICE_STATE || path.join(os.homedir(), '.openzoo', 'voice.json');
|
|
52
49
|
const CARD_FILE = process.env.OPENZOO_VOICE_CARD || path.join(os.homedir(), '.openzoo', 'voice-card.md');
|
|
@@ -217,7 +214,7 @@ async function bindGateway(corpus, log, label) {
|
|
|
217
214
|
for (let i = 0; i < parts.length; i++) {
|
|
218
215
|
const r = await fetch(`${GATEWAY}/v1/hrr/bind`, {
|
|
219
216
|
method: 'POST',
|
|
220
|
-
headers: { 'content-type': 'application/json' },
|
|
217
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
221
218
|
body: JSON.stringify(contextId ? { context_id: contextId, corpus: parts[i] } : { corpus: parts[i] }),
|
|
222
219
|
});
|
|
223
220
|
if (!r.ok) throw new Error(`gateway bind ${label} ${i + 1}/${parts.length}: HTTP ${r.status}`);
|
|
@@ -228,7 +225,9 @@ async function bindGateway(corpus, log, label) {
|
|
|
228
225
|
}
|
|
229
226
|
|
|
230
227
|
/**
|
|
231
|
-
*
|
|
228
|
+
* Scored item bind through the wallet-authenticated gateway.
|
|
229
|
+
* Legacy daemonCtx state fields now refer to private gateway contexts;
|
|
230
|
+
* old daemon contexts must be re-ingested.
|
|
232
231
|
*
|
|
233
232
|
* ONE ITEM PER TURN, deliberately: the daemon indexes discrete items, so a
|
|
234
233
|
* turn stays whole and recall returns something imitable. Chunking a
|
|
@@ -247,11 +246,10 @@ async function bindDaemon(turns, log, label) {
|
|
|
247
246
|
text: renderTurn(t),
|
|
248
247
|
metadata: { source: t.source, where: t.where, when: t.when },
|
|
249
248
|
}));
|
|
250
|
-
const r = await fetch(`${
|
|
249
|
+
const r = await fetch(`${config.apiBase}/v1/hrr/bind`, {
|
|
251
250
|
method: 'POST',
|
|
252
|
-
headers: { 'content-type': 'application/json'
|
|
251
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
253
252
|
body: JSON.stringify({
|
|
254
|
-
tenant_id: DAEMON_TENANT,
|
|
255
253
|
items,
|
|
256
254
|
...(contextId ? { context_id: contextId } : {}),
|
|
257
255
|
}),
|
|
@@ -374,10 +372,10 @@ export async function distillStyleCard(turns, log = () => {}) {
|
|
|
374
372
|
// ---------------------------------------------------------------- recall
|
|
375
373
|
|
|
376
374
|
async function daemonRecall(contextId, query, topK) {
|
|
377
|
-
const r = await fetch(`${
|
|
375
|
+
const r = await fetch(`${config.apiBase}/v1/hrr/recall`, {
|
|
378
376
|
method: 'POST',
|
|
379
|
-
headers: { 'content-type': 'application/json'
|
|
380
|
-
body: JSON.stringify({
|
|
377
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
378
|
+
body: JSON.stringify({ context_id: contextId, query, top_k: topK }),
|
|
381
379
|
});
|
|
382
380
|
if (!r.ok) throw new Error(`recall HTTP ${r.status}`);
|
|
383
381
|
return (await r.json()).items || [];
|
package/lib/xbot.js
CHANGED
|
@@ -22,6 +22,7 @@ import os from 'node:os';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import crypto from 'node:crypto';
|
|
24
24
|
import { FUNDING_ASSETS } from './config.js';
|
|
25
|
+
import { withNamespace, withQuoteNamespace } from './namespace.js';
|
|
25
26
|
import { deriveBurner } from './xburner.js';
|
|
26
27
|
|
|
27
28
|
const GATEWAY = process.env.OPENZOO_GATEWAY || 'https://x402-tokens.fly.dev';
|
|
@@ -214,7 +215,7 @@ async function openzooFacts() {
|
|
|
214
215
|
try {
|
|
215
216
|
const r = await fetch(`${GATEWAY}/v1/chat/completions`, {
|
|
216
217
|
method: 'POST',
|
|
217
|
-
headers: { 'content-type': 'application/json' },
|
|
218
|
+
headers: withQuoteNamespace({ 'content-type': 'application/json' }),
|
|
218
219
|
body: JSON.stringify({ model: 'openai/gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
|
|
219
220
|
});
|
|
220
221
|
const j = await r.json().catch(() => ({}));
|
|
@@ -269,7 +270,7 @@ export async function ensureSharedContext(state) {
|
|
|
269
270
|
const facts = await openzooFacts();
|
|
270
271
|
const res = await fetch(`${GATEWAY}/v1/hrr/bind`, {
|
|
271
272
|
method: 'POST',
|
|
272
|
-
headers: { 'content-type': 'application/json' },
|
|
273
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
273
274
|
body: JSON.stringify({ corpus: `openzoobot shared corpus. Threads the bot reads are appended here.\n\n${facts}` }),
|
|
274
275
|
});
|
|
275
276
|
if (!res.ok) throw new Error(`bind ${res.status}: ${(await res.text()).slice(0, 160)}`);
|
|
@@ -661,7 +662,7 @@ export async function seedFromMentions(creds, contextId, { maxPages = 10 } = {})
|
|
|
661
662
|
|
|
662
663
|
const b = await fetch(`${GATEWAY}/v1/hrr/bind`, {
|
|
663
664
|
method: 'POST',
|
|
664
|
-
headers: { 'content-type': 'application/json' },
|
|
665
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
665
666
|
body: JSON.stringify({ context_id: contextId, corpus }),
|
|
666
667
|
});
|
|
667
668
|
if (b.ok) {
|
|
@@ -796,7 +797,7 @@ export async function bindThread(contextId, chain, mention) {
|
|
|
796
797
|
try {
|
|
797
798
|
const res = await fetch(`${GATEWAY}/v1/hrr/bind`, {
|
|
798
799
|
method: 'POST',
|
|
799
|
-
headers: { 'content-type': 'application/json' },
|
|
800
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
800
801
|
body: JSON.stringify({ context_id: contextId, corpus }),
|
|
801
802
|
});
|
|
802
803
|
if (!res.ok) return 0;
|
|
@@ -1080,14 +1081,14 @@ export async function askZoo(question, { key, maxTokens = ANSWER_TOKENS, thread
|
|
|
1080
1081
|
call: async (body) => {
|
|
1081
1082
|
const res = await fetch(`${FREE_GATEWAY}/v1/chat/completions`, {
|
|
1082
1083
|
method: 'POST',
|
|
1083
|
-
headers: {
|
|
1084
|
+
headers: withNamespace({
|
|
1084
1085
|
'content-type': 'application/json',
|
|
1085
1086
|
// NO x-hrr-top-k. The gateway already scales breadth to the corpus
|
|
1086
1087
|
// (scaleTopK), and a client-sent X-HRR-Top-K "wins over everything" —
|
|
1087
1088
|
// pinning a number replaces a curve that grows with the thread.
|
|
1088
1089
|
...(contextId ? { 'x-hrr-context': contextId } : {}),
|
|
1089
1090
|
...(key ? { authorization: `Bearer ${key}` } : {}),
|
|
1090
|
-
},
|
|
1091
|
+
}),
|
|
1091
1092
|
body: JSON.stringify(body),
|
|
1092
1093
|
});
|
|
1093
1094
|
const json = await res.json().catch(() => ({}));
|
|
@@ -1180,7 +1181,6 @@ const TOPUP_CAP_USD = Number(process.env.OPENZOO_XBOT_TOPUP_CAP || 500);
|
|
|
1180
1181
|
|
|
1181
1182
|
export async function ensureCredit(burner) {
|
|
1182
1183
|
const { PayClient } = await import('./pay.js');
|
|
1183
|
-
const { withNamespace } = await import('./namespace.js');
|
|
1184
1184
|
const ns = (h) => withNamespace(h, { keypair: burner.keypair });
|
|
1185
1185
|
|
|
1186
1186
|
let balance = 0;
|
|
@@ -1258,9 +1258,9 @@ export async function askZooPaid(question, { burner, thread = '', maxTokens = AN
|
|
|
1258
1258
|
messages,
|
|
1259
1259
|
maxTokens,
|
|
1260
1260
|
allowTools: Boolean(WEB_SEARCH),
|
|
1261
|
-
//
|
|
1262
|
-
//
|
|
1263
|
-
call: async (body) => (await pay.chat(body
|
|
1261
|
+
// The operator's archive belongs to its signer. Burner calls use only
|
|
1262
|
+
// the explicitly supplied thread; they cannot attach that private context.
|
|
1263
|
+
call: async (body) => (await pay.chat(body)).data,
|
|
1264
1264
|
}).then((result) => ({
|
|
1265
1265
|
...result,
|
|
1266
1266
|
// Every on-chain settle signature this answer paid with, so the PAID log
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.19",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|