joinhive 2.0.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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/bin/hive +820 -0
  4. package/bin/hive-claim-invite.mjs +88 -0
  5. package/bin/hive-join.mjs +243 -0
  6. package/bin/hive-keygen.mjs +48 -0
  7. package/bin/hive-mint.mjs +65 -0
  8. package/bin/hive-net.mjs +400 -0
  9. package/bin/hive-wallet.mjs +120 -0
  10. package/bin/hived.mjs +5 -0
  11. package/bin/setup-queen.sh +71 -0
  12. package/daemon/engines/anthropic.mjs +45 -0
  13. package/daemon/engines/cli.mjs +25 -0
  14. package/daemon/engines/index.mjs +84 -0
  15. package/daemon/engines/openai.mjs +42 -0
  16. package/daemon/fanout.mjs +47 -0
  17. package/daemon/hived.mjs +782 -0
  18. package/daemon/relay/client.mjs +196 -0
  19. package/daemon/relay/cursor.mjs +59 -0
  20. package/daemon/relay/ws.mjs +100 -0
  21. package/dev/compose.yml +109 -0
  22. package/docs/README.md +30 -0
  23. package/docs/SUMMARY.md +20 -0
  24. package/docs/a2a-events.md +82 -0
  25. package/docs/architecture.md +86 -0
  26. package/docs/cli.md +70 -0
  27. package/docs/concepts.md +50 -0
  28. package/docs/contracts.md +85 -0
  29. package/docs/http-api.md +78 -0
  30. package/docs/protocols.md +64 -0
  31. package/docs/quickstart.md +51 -0
  32. package/docs/security.md +53 -0
  33. package/docs/self-hosting.md +101 -0
  34. package/docs/tokenomics.md +63 -0
  35. package/install-remote.sh +49 -0
  36. package/join.sh +81 -0
  37. package/onchain/deploy-v2.sh +82 -0
  38. package/onchain/deployments.sepolia.json +14 -0
  39. package/onchain/foundry.toml +11 -0
  40. package/onchain/migrate-v2.mjs +76 -0
  41. package/onchain/src/Honey.sol +45 -0
  42. package/onchain/src/HoneyV2.sol +74 -0
  43. package/onchain/src/Jelly.sol +19 -0
  44. package/onchain/src/JellyV2.sol +31 -0
  45. package/package.json +72 -0
  46. package/protocols/book-recs.md +11 -0
  47. package/protocols/email-in-style.md +15 -0
  48. package/protocols/event-hunt.md +17 -0
  49. package/protocols/food-order.md +20 -0
  50. package/protocols/group-diagnosis.md +13 -0
  51. package/protocols/meta.md +11 -0
  52. package/protocols/movie-recs.md +17 -0
  53. package/protocols/predict.md +21 -0
  54. package/protocols/read-what-others-read.md +14 -0
  55. package/protocols/session-bounty.md +11 -0
  56. package/protocols/session-split-pool.md +10 -0
  57. package/server/Dockerfile +33 -0
  58. package/server/api.mjs +192 -0
  59. package/server/join-page.mjs +169 -0
  60. package/server/keygen-treasury.mjs +33 -0
  61. package/server/provision.mjs +262 -0
  62. package/server/rewarder.mjs +369 -0
  63. package/server/supervisor.mjs +237 -0
  64. package/server/treasury.mjs +172 -0
  65. package/shared/config-schema.mjs +94 -0
  66. package/shared/events.mjs +47 -0
  67. package/shared/nip-oa.mjs +56 -0
  68. package/shared/nip98.mjs +41 -0
  69. package/shared/redact.mjs +20 -0
  70. package/shared/rewards.json +33 -0
  71. package/shared/sealed.mjs +50 -0
  72. package/shared/txqueue.mjs +42 -0
  73. package/skills/hive-capability-store/SKILL.md +49 -0
  74. package/skills/hive-data-store/SKILL.md +60 -0
  75. package/skills/hive-join/SKILL.md +86 -0
  76. package/skills/hive-object-store/SKILL.md +45 -0
  77. package/skills/hive-prompt/SKILL.md +54 -0
  78. package/skills/hive-protocol-author/SKILL.md +92 -0
  79. package/skills/hive-wallet/SKILL.md +54 -0
  80. package/watcher/distill.mjs +248 -0
  81. package/watcher/global.nfh.hive.sync.plist.tmpl +20 -0
  82. package/watcher/sync.mjs +136 -0
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ // server/treasury — the bee-host's money worker. Holds MINTER_ROLE only:
3
+ // worst-case compromise = testnet token inflation, revocable by the admin's
4
+ // laptop key. It never touches member wallet keys.
5
+ //
6
+ // 1. Genesis grants: consumes /data/treasury-queue.jsonl (written by the
7
+ // provisioner) — 500 JELLY mint + 0.05 ETH drip per new member wallet.
8
+ // Ledgered in /data/treasury-ledger.json BEFORE broadcasting: re-runs
9
+ // never double-grant.
10
+ // 2. Gas top-ups: hourly sweep of registry wallets, drip when < 0.01 ETH.
11
+ // 3. Alerts: posts to #hive-lounge (as the steward identity) when the
12
+ // treasury float runs low.
13
+ // 4. M3 hook: the daily HONEY epoch rewarder will live here too.
14
+ import { readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { JsonRpcProvider, Wallet, Contract, parseUnits, parseEther, formatEther } from 'ethers';
17
+ import { getPublicKey } from 'nostr-tools/pure';
18
+ import { TxQueue } from '../shared/txqueue.mjs';
19
+ import { RelayClient } from '../daemon/relay/client.mjs';
20
+ import { EV } from '../shared/events.mjs';
21
+ import { runEpoch, REWARDS } from './rewarder.mjs';
22
+
23
+ const DATA_DIR = process.env.HIVE_DATA || '/data';
24
+ const RPC = process.env.SEPOLIA_RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com';
25
+ const RELAY_URL = (process.env.HIVE_RELAY_URL || process.env.BUZZ_RELAY_URL || 'http://localhost:3000');
26
+ const TREASURY_KEY = process.env.TREASURY_PRIVATE_KEY || '';
27
+ const STEWARD = process.env.HIVE_STEWARD_KEY || '';
28
+ const PACK_DIR = new URL('..', import.meta.url).pathname;
29
+
30
+ const TOPUP_BELOW_ETH = 0.01;
31
+ const TOPUP_AMOUNT_ETH = 0.03;
32
+ const ALERT_BELOW_ETH = 0.2;
33
+ const TICK_MS = 60_000;
34
+ const TOPUP_EVERY_MS = 60 * 60_000;
35
+
36
+ const log = (...a) => console.log(`[treasury ${new Date().toISOString()}]`, ...a);
37
+ const loadJson = (p, fb) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return fb; } };
38
+ const writeAtomic = (p, s) => { const t = `${p}.tmp`; writeFileSync(t, s); renameSync(t, p); };
39
+
40
+ if (!TREASURY_KEY) { console.error('[treasury] FATAL: TREASURY_PRIVATE_KEY required'); process.exit(1); }
41
+ const deployments = loadJson(join(PACK_DIR, 'onchain', 'deployments.sepolia.json'), {});
42
+ if (!deployments.jelly) { console.error('[treasury] FATAL: no deployments.sepolia.json'); process.exit(1); }
43
+
44
+ const provider = new JsonRpcProvider(RPC);
45
+ const wallet = new Wallet(TREASURY_KEY, provider);
46
+ const txq = new TxQueue(wallet);
47
+ const jelly = new Contract(deployments.jelly, ['function mint(address,uint256)', 'function balanceOf(address) view returns (uint256)'], wallet);
48
+ const relay = STEWARD ? new RelayClient({ relayUrl: RELAY_URL, privkey: STEWARD, log }) : null;
49
+
50
+ const ledgerPath = join(DATA_DIR, 'treasury-ledger.json');
51
+ const queuePath = join(DATA_DIR, 'treasury-queue.jsonl');
52
+ const ledger = loadJson(ledgerPath, {});
53
+ const saveLedger = () => writeAtomic(ledgerPath, JSON.stringify(ledger, null, 2));
54
+
55
+ let loungeId = null;
56
+ const alert = async (text) => {
57
+ log('ALERT:', text);
58
+ try {
59
+ if (!relay) return;
60
+ if (!loungeId) loungeId = await relay.ensureChannel('hive-lounge');
61
+ await relay.sendMessage(loungeId, `🏦 treasury: ${text}`);
62
+ } catch (e) { log('alert post failed:', e.message); }
63
+ };
64
+
65
+ let lastAlertAt = 0;
66
+ const checkFloat = async () => {
67
+ const bal = await provider.getBalance(wallet.address);
68
+ if (bal < parseEther(String(ALERT_BELOW_ETH)) && Date.now() - lastAlertAt > 6 * 3600_000) {
69
+ lastAlertAt = Date.now();
70
+ await alert(`float low: ${formatEther(bal)} ETH at ${wallet.address} — refill from a Sepolia faucet (pk910 PoW works headless).`);
71
+ }
72
+ return bal;
73
+ };
74
+
75
+ // ---- genesis grants ------------------------------------------------------------
76
+ const processQueue = async () => {
77
+ if (!existsSync(queuePath)) return;
78
+ const lines = readFileSync(queuePath, 'utf8').split('\n').filter(Boolean);
79
+ for (const line of lines) {
80
+ let job; try { job = JSON.parse(line); } catch { continue; }
81
+ if (job.kind !== 'genesis' || !/^0x[0-9a-fA-F]{40}$/.test(job.evm || '')) continue;
82
+ const key = `genesis:${job.bee}`;
83
+ if (ledger[key]?.done) continue;
84
+ ledger[key] = ledger[key] || { evm: job.evm, at: Math.floor(Date.now() / 1000) };
85
+ try {
86
+ if (!ledger[key].jelly_tx) {
87
+ // Ledger the intent BEFORE broadcast; a crash between write and
88
+ // receipt risks one manual check, never a silent double-mint loop.
89
+ ledger[key].jelly_pending = true; saveLedger();
90
+ const r = await txq.enqueue((o) => jelly.mint(job.evm, parseUnits(String(job.jelly || 500), 18), o));
91
+ ledger[key].jelly_tx = r.hash; delete ledger[key].jelly_pending; saveLedger();
92
+ log(`genesis ${job.bee}: minted ${job.jelly} JELLY → ${job.evm} (${r.hash.slice(0, 12)})`);
93
+ }
94
+ if (!ledger[key].eth_tx) {
95
+ ledger[key].eth_pending = true; saveLedger();
96
+ const r = await txq.enqueue((o) => wallet.sendTransaction({ to: job.evm, value: parseEther(String(job.eth || 0.05)), ...o }));
97
+ ledger[key].eth_tx = r.hash; delete ledger[key].eth_pending; saveLedger();
98
+ log(`genesis ${job.bee}: dripped ${job.eth} ETH → ${job.evm} (${r.hash.slice(0, 12)})`);
99
+ }
100
+ ledger[key].done = true; saveLedger();
101
+ await alert(`welcomed ${job.bee}: 500 JELLY + gas delivered to ${job.evm.slice(0, 10)}…`);
102
+ } catch (e) {
103
+ log(`genesis ${job.bee} failed (will retry next tick): ${String(e.message).slice(0, 160)}`);
104
+ }
105
+ }
106
+ };
107
+
108
+ // ---- gas top-ups ------------------------------------------------------------------
109
+ let lastTopup = 0;
110
+ const topUps = async () => {
111
+ if (Date.now() - lastTopup < TOPUP_EVERY_MS) return;
112
+ lastTopup = Date.now();
113
+ const reg = loadJson(join(DATA_DIR, 'registry.json'), {});
114
+ const evms = [...new Set(Object.values(reg).map((r) => r.evm).filter((e) => /^0x[0-9a-fA-F]{40}$/.test(e || '')))];
115
+ for (const evm of evms) {
116
+ try {
117
+ const bal = await provider.getBalance(evm);
118
+ if (bal < parseEther(String(TOPUP_BELOW_ETH))) {
119
+ const r = await txq.enqueue((o) => wallet.sendTransaction({ to: evm, value: parseEther(String(TOPUP_AMOUNT_ETH)), ...o }));
120
+ log(`top-up: ${TOPUP_AMOUNT_ETH} ETH → ${evm} (${r.hash.slice(0, 12)})`);
121
+ }
122
+ } catch (e) { log(`top-up ${evm} failed: ${String(e.message).slice(0, 120)}`); }
123
+ }
124
+ };
125
+
126
+ // ---- the daily HONEY epoch (rewarder) --------------------------------------------
127
+ // Epoch D closes at rewards.epoch_close_utc_hour on day D (≈ midnight IST).
128
+ // One run per date, ledgered; a crash mid-run resumes losslessly because the
129
+ // receipt precedes the mints and TxQueue awaits receipts.
130
+ const honey = deployments.honey ? new Contract(deployments.honey, ['function mint(address,uint256)'], wallet) : null;
131
+ const rewarderStatePath = join(DATA_DIR, 'rewarder-state.json');
132
+ const closedEpochDate = () => {
133
+ const nowMs = Date.now() - REWARDS.epoch_close_utc_hour * 3600_000;
134
+ return new Date(nowMs - 86400_000 * 0).toISOString().slice(0, 10); // most recent day whose close has passed
135
+ };
136
+ const maybeRunEpoch = async () => {
137
+ // v2 contracts only — the treasury key has no mint rights on v1 (Ownable).
138
+ if (!honey || !relay || deployments.version !== 2) return;
139
+ const date = closedEpochDate();
140
+ const key = `epoch:${date}`;
141
+ if (ledger[key]?.done) return;
142
+ const reg = loadJson(join(DATA_DIR, 'registry.json'), {});
143
+ if (!Object.values(reg).some((r) => r?.is_bee)) return; // nothing to reward yet
144
+ const state = loadJson(rewarderStatePath, {});
145
+ if (!loungeId) { try { loungeId = await relay.ensureChannel('hive-lounge'); } catch {} }
146
+ const logsId = await relay.ensureChannel('hive-logs');
147
+ ledger[key] = { started_at: Math.floor(Date.now() / 1000) }; saveLedger();
148
+ const { mints, txs } = await runEpoch(date, {
149
+ relay, logsChannelId: logsId, registry: reg, state,
150
+ honeyContract: honey, txq, parseUnits, log,
151
+ selfPubkey: getPublicKey(Uint8Array.from(Buffer.from(STEWARD, 'hex'))),
152
+ emit: (obj) => relay.sendMessage(logsId, JSON.stringify(obj)),
153
+ });
154
+ writeAtomic(rewarderStatePath, JSON.stringify(state, null, 2));
155
+ ledger[key] = { ...ledger[key], done: true, mints: mints.length, minted: txs.length }; saveLedger();
156
+ if (mints.length) await alert(`epoch ${date}: ${txs.length}/${mints.length} HONEY mints executed — hive leaderboard to see the ranks.`);
157
+ };
158
+
159
+ const main = async () => {
160
+ const bal = await provider.getBalance(wallet.address).catch(() => null);
161
+ log(`treasury up: ${wallet.address}, float ${bal === null ? '?' : formatEther(bal)} ETH, jelly ${deployments.jelly}, honey ${deployments.honey || '(v1 — no rewarder)'}${deployments.version === 2 ? ' [v2]' : ''}`);
162
+ for (;;) {
163
+ try {
164
+ await processQueue();
165
+ await topUps();
166
+ await checkFloat();
167
+ await maybeRunEpoch();
168
+ } catch (e) { log('tick error:', String(e.message).slice(0, 160)); }
169
+ await new Promise((r) => setTimeout(r, TICK_MS));
170
+ }
171
+ };
172
+ main();
@@ -0,0 +1,94 @@
1
+ // shared/config-schema — validated config for endpoints and bees.
2
+ //
3
+ // The old daemon worked only because defaults papered over missing fields
4
+ // (e.g. ~/.hive/config.json omits compute_model). At 15 bees a silent
5
+ // misconfig is a silent outage: validate loud at boot/spawn instead.
6
+
7
+ export const PROVIDERS = Object.freeze(['anthropic', 'openai', 'openrouter', 'hermes', 'echo', 'cli']);
8
+
9
+ // Providers with an OpenAI-compatible /chat/completions surface.
10
+ export const OPENAI_COMPAT_BASES = Object.freeze({
11
+ openai: 'https://api.openai.com/v1',
12
+ openrouter: 'https://openrouter.ai/api/v1',
13
+ hermes: 'https://inference-api.nousresearch.com/v1',
14
+ });
15
+
16
+ // Default model tiers. extract = cheap/fast, compute = strong (user-facing).
17
+ export const MODEL_TIERS = Object.freeze({
18
+ anthropic: { extract: 'claude-haiku-4-5', compute: 'claude-sonnet-5' },
19
+ openai: { extract: 'gpt-5-mini', compute: 'gpt-5' },
20
+ // OpenRouter/Hermes route many models — require an explicit choice rather
21
+ // than guessing a slug that may not exist for this member's account.
22
+ openrouter: { extract: null, compute: null },
23
+ hermes: { extract: null, compute: null },
24
+ });
25
+
26
+ export const DEFAULTS = Object.freeze({
27
+ poll_secs: 10,
28
+ relay: 'http://localhost:3000',
29
+ role: 'endpoint', // 'endpoint' (laptop) | 'bee' (cloud, owned by a human)
30
+ provider: 'cli', // legacy laptop default; bees use API providers
31
+ engine: 'claude', // cli provider only: argv0
32
+ engine_args: ['-p', '--model', 'haiku'],
33
+ compute_model: 'sonnet', // cli provider only: --model override for compute
34
+ share_intent: true,
35
+ rate_limit_per_min: 5,
36
+ mute_cooldown_min: 30,
37
+ channels: { lounge: 'hive-lounge', intents: 'hive-intents', logs: 'hive-logs' },
38
+ fanout: { top_k: 3, max_results_per_tick: 3, max_results_per_day: 40 },
39
+ spend: { jelly_daily_cap: 15, per_tx_cap: 10, tx_per_hour_cap: 6 },
40
+ });
41
+
42
+ const clamp = (n, lo, hi, dflt) => {
43
+ const v = Number(n);
44
+ return Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : dflt;
45
+ };
46
+
47
+ // validateConfig(raw, {requireBee}) -> { config, errors: string[] }
48
+ // Merges defaults, normalizes numbers, and collects hard errors. `requireBee`
49
+ // is set by the supervisor: a bee must name its owner and use an API provider.
50
+ export const validateConfig = (raw = {}, { requireBee = false } = {}) => {
51
+ const errors = [];
52
+ const cfg = {
53
+ ...DEFAULTS,
54
+ ...raw,
55
+ channels: { ...DEFAULTS.channels, ...(raw.channels || {}) },
56
+ fanout: { ...DEFAULTS.fanout, ...(raw.fanout || {}) },
57
+ spend: { ...DEFAULTS.spend, ...(raw.spend || {}) },
58
+ };
59
+
60
+ cfg.poll_secs = clamp(cfg.poll_secs, 5, 60, DEFAULTS.poll_secs);
61
+ cfg.rate_limit_per_min = clamp(cfg.rate_limit_per_min, 1, 60, DEFAULTS.rate_limit_per_min);
62
+ cfg.mute_cooldown_min = clamp(cfg.mute_cooldown_min, 1, 24 * 60, DEFAULTS.mute_cooldown_min);
63
+ cfg.fanout.top_k = clamp(cfg.fanout.top_k, 1, 15, DEFAULTS.fanout.top_k);
64
+ cfg.fanout.max_results_per_tick = clamp(cfg.fanout.max_results_per_tick, 1, 10, DEFAULTS.fanout.max_results_per_tick);
65
+ cfg.fanout.max_results_per_day = clamp(cfg.fanout.max_results_per_day, 1, 500, DEFAULTS.fanout.max_results_per_day);
66
+ cfg.spend.jelly_daily_cap = clamp(cfg.spend.jelly_daily_cap, 0, 1000, DEFAULTS.spend.jelly_daily_cap);
67
+ cfg.spend.per_tx_cap = clamp(cfg.spend.per_tx_cap, 0, 100, DEFAULTS.spend.per_tx_cap);
68
+ cfg.spend.tx_per_hour_cap = clamp(cfg.spend.tx_per_hour_cap, 0, 60, DEFAULTS.spend.tx_per_hour_cap);
69
+
70
+ if (!PROVIDERS.includes(cfg.provider)) errors.push(`provider must be one of ${PROVIDERS.join('|')}, got "${cfg.provider}"`);
71
+ if (typeof cfg.relay !== 'string' || !/^https?:\/\//.test(cfg.relay)) errors.push(`relay must be an http(s) URL, got "${cfg.relay}"`);
72
+ if (cfg.role !== 'endpoint' && cfg.role !== 'bee') errors.push(`role must be endpoint|bee, got "${cfg.role}"`);
73
+
74
+ const isApiProvider = !['echo', 'cli'].includes(cfg.provider);
75
+ if (isApiProvider) {
76
+ const tiers = MODEL_TIERS[cfg.provider] || { extract: null, compute: null };
77
+ cfg.model_extract = cfg.model_extract || tiers.extract;
78
+ cfg.model_compute = cfg.model_compute || tiers.compute;
79
+ if (!cfg.model_extract || !cfg.model_compute) {
80
+ errors.push(`provider "${cfg.provider}" needs explicit model_extract and model_compute in config.json`);
81
+ }
82
+ if (!cfg.base_url) cfg.base_url = OPENAI_COMPAT_BASES[cfg.provider] || null;
83
+ }
84
+
85
+ if (requireBee || cfg.role === 'bee') {
86
+ if (!/^[0-9a-f]{64}$/i.test(cfg.owner_pubkey || '')) errors.push('bee config needs owner_pubkey (64-hex nostr pubkey of the human)');
87
+ if (!cfg.owner_name) errors.push('bee config needs owner_name');
88
+ if (!cfg.bee_name) errors.push('bee config needs bee_name (e.g. "avdhesh.bee")');
89
+ // 'echo' stays allowed: it is the test seam (thinks nothing, spends nothing).
90
+ if (cfg.provider === 'cli') errors.push('a bee cannot use provider "cli" — cloud bees need an API provider (anthropic|openai|openrouter|hermes)');
91
+ }
92
+
93
+ return { config: cfg, errors };
94
+ };
@@ -0,0 +1,47 @@
1
+ // shared/events — the Hive typed-event vocabulary.
2
+ //
3
+ // ALL machine-readable JSON rides one channel (#hive-logs), keyed by `type`.
4
+ // This file is the single place the vocabulary lives; daemon, CLI, server,
5
+ // and rewarder must import from here rather than re-typing strings.
6
+ //
7
+ // Invariant (R-B1): an event's `by` field must equal the nostr pubkey that
8
+ // SIGNED the message, or the event is a spoof and must be dropped.
9
+
10
+ export const EV = Object.freeze({
11
+ INTENT: 'hive-intent', // {intent, origin?, source_event?, for, by}
12
+ RESULT: 'hive-result', // {intent_event, intent, result, for, by, sources, engine, protocols_used}
13
+ NEED: 'hive-need', // {intent, for, by} — intent the network couldn't serve
14
+ PROTOCOL: 'hive-protocol', // {name, match, body, by} | {name, tombstone:true, by}
15
+ FEEDBACK: 'hive-feedback', // {result, result_by, dir:'up'|'down', note?, by, at} — HUMAN CLI only; the daemon must NEVER emit this (HONEY minting depends on it)
16
+ WALLET: 'hive-wallet', // {pubkey, evm, solana} — public address announcement
17
+ SESSION: 'hive-session', // {session_id, kind, prompt, deadline, quorum, resolver, pool?, payout_mode?, by}
18
+ OFFER: 'hive-offer', // {session_id, offer, by}
19
+ SETTLE: 'hive-settle', // {session_id, kind, result, status, offers, quorum, pool, payout_mode, payout[], by}
20
+ TRANSFER: 'hive-transfer', // {object, name, from, to, at} — PoW object gift (human-gated)
21
+ REPORT: 'hive-report', // {subject, reason?, by, at}
22
+ PROPOSAL: 'hive-proposal', // {proposal_id, text, by, at}
23
+ VOTE: 'hive-vote', // {proposal_id, choice:'yes'|'no', by, at}
24
+ JOIN: 'hive-join', // {name, owner_pubkey?, is_bee?, by} — membership announcement
25
+ SKILL: 'hive-skill', // {name, description, price_hint?, by} — capability announcement
26
+ // New in v2 (15-member network):
27
+ TIP: 'hive-tip', // {from, to, amount, token:'JELLY', tx, by} — on-chain transfer receipt
28
+ SPEND: 'hive-spend', // {reason, to, amount, tx, idempotency_key, by} — bee budgeted-spend receipt
29
+ MUTE: 'hive-mute', // {subject, until, by} — daemon broadcasts mutes it applies
30
+ EPOCH: 'hive-epoch', // {epoch, mints[], penalties[], txs[], by} — rewarder receipt
31
+ DND: 'hive-dnd', // {on:true|false, price?, by}
32
+ DIGEST: 'hive-digest', // {title?, summary, participants?, by}
33
+ CONTROL: 'hive-control', // {action:'pause'|'resume', bee, by} — OWNER-signed kill switch for their own bee
34
+ ALTKEY: 'hive-altkey', // mutual device linking: {alt, by:member} claim + {owner, by:alt} ack — linked only when BOTH exist; {…, revoke:true} from either side unlinks. Rewarder treats linked keys as ONE member.
35
+ });
36
+
37
+ export const tryJson = (s) => { try { return JSON.parse(s); } catch { return null; } };
38
+
39
+ // R-B1 provenance: parse a relay message into a verified event, or null.
40
+ // `msg` = {content, pubkey, id, created_at}. A `by` that contradicts the
41
+ // signer is a spoof; a missing `by` is tolerated (signer is the author).
42
+ export const verifiedEvent = (msg) => {
43
+ const j = tryJson(msg && msg.content);
44
+ if (!j || typeof j !== 'object') return null;
45
+ if (j.by && j.by !== msg.pubkey) return null;
46
+ return j;
47
+ };
@@ -0,0 +1,56 @@
1
+ // shared/nip-oa — NIP-OA Owner Attestation (Buzz).
2
+ //
3
+ // An auth tag proves an OWNER key authorized an AGENT key, so Buzz clients
4
+ // (desktop Agents tab, `buzz users get --owner`) can verify a bee belongs to
5
+ // its member. Format and preimage mirror buzz-sdk/src/nip_oa.rs exactly:
6
+ //
7
+ // tag = ["auth", <owner-pubkey-hex>, <conditions>, <sig-hex>]
8
+ // preimage = "nostr:agent-auth:" || agent_pubkey_hex || ":" || conditions
9
+ // sig = BIP-340 Schnorr(SHA256(preimage), owner_secret_key)
10
+ //
11
+ // Conditions "" (empty) means the attestation covers every event the agent
12
+ // signs — it is what hive uses, so the same tag also satisfies the CLI's
13
+ // per-event condition check on the bee's kind-0 profile.
14
+ import { schnorr } from '@noble/curves/secp256k1.js';
15
+ import { sha256 } from '@noble/hashes/sha2.js';
16
+ import { getPublicKey } from 'nostr-tools/pure';
17
+
18
+ const HEX64 = /^[0-9a-f]{64}$/;
19
+ const HEX128 = /^[0-9a-f]{128}$/;
20
+
21
+ const preimageHash = (agentPubkeyHex, conditions) =>
22
+ sha256(new TextEncoder().encode(`nostr:agent-auth:${agentPubkeyHex}:${conditions}`));
23
+
24
+ // Sign an auth tag with the owner's nostr privkey (64-hex). Returns the tag
25
+ // as an array, ready to be placed in a kind-0 `tags` list or JSON body.
26
+ export const computeAuthTag = (ownerPrivkeyHex, agentPubkeyHex, conditions = '') => {
27
+ const agent = String(agentPubkeyHex).toLowerCase();
28
+ if (!HEX64.test(agent)) throw new Error('agent pubkey must be 64 lowercase hex chars');
29
+ const sk = Uint8Array.from(Buffer.from(ownerPrivkeyHex, 'hex'));
30
+ const owner = getPublicKey(sk);
31
+ if (owner === agent) throw new Error('owner and agent pubkeys must differ (self-attestation rejected)');
32
+ const sig = Buffer.from(schnorr.sign(preimageHash(agent, String(conditions)), sk)).toString('hex');
33
+ return ['auth', owner, String(conditions), sig];
34
+ };
35
+
36
+ // Verify a tag (array form) against the agent it should authorize.
37
+ // Returns the owner pubkey hex on success, null on any failure.
38
+ export const verifyAuthTag = (tag, agentPubkeyHex) => {
39
+ if (!Array.isArray(tag) || tag.length !== 4 || tag[0] !== 'auth') return null;
40
+ const owner = String(tag[1]);
41
+ const conditions = String(tag[2]);
42
+ const sig = String(tag[3]);
43
+ const agent = String(agentPubkeyHex).toLowerCase();
44
+ if (!HEX64.test(owner) || !HEX128.test(sig) || !HEX64.test(agent)) return null;
45
+ if (owner === agent) return null;
46
+ try {
47
+ const ok = schnorr.verify(
48
+ Uint8Array.from(Buffer.from(sig, 'hex')),
49
+ preimageHash(agent, conditions),
50
+ Uint8Array.from(Buffer.from(owner, 'hex')),
51
+ );
52
+ return ok ? owner : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ };
@@ -0,0 +1,41 @@
1
+ // shared/nip98 — NIP-98 HTTP auth for the Buzz relay bridge.
2
+ //
3
+ // Extracted from bin/hive-claim-invite.mjs so the daemon's relay client and
4
+ // the provisioning CLI sign requests the same way. A NIP-98 header is a
5
+ // kind:27235 event over (url, method[, payload sha256]), base64-encoded into
6
+ // `Authorization: Nostr <b64>`. The relay verifies signer, url, method,
7
+ // payload hash, and created_at skew.
8
+ import { createHash, randomUUID } from 'node:crypto';
9
+ import { finalizeEvent } from 'nostr-tools/pure';
10
+
11
+ export const sha256hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
12
+
13
+ // privkey: 64-hex string. bodyBytes: Buffer|Uint8Array|undefined.
14
+ export const nip98Header = (privkey, method, url, bodyBytes) => {
15
+ const sk = privkey instanceof Uint8Array ? privkey : Uint8Array.from(Buffer.from(privkey, 'hex'));
16
+ const tags = [
17
+ ['u', url],
18
+ ['method', method],
19
+ ['nonce', randomUUID()],
20
+ ];
21
+ if (bodyBytes && bodyBytes.length) tags.push(['payload', sha256hex(bodyBytes)]);
22
+ const evt = finalizeEvent({ kind: 27235, created_at: Math.floor(Date.now() / 1000), tags, content: '' }, sk);
23
+ return 'Nostr ' + Buffer.from(JSON.stringify(evt)).toString('base64');
24
+ };
25
+
26
+ // Signed fetch against the relay bridge. Returns { status, json } and never
27
+ // throws on HTTP-level errors (network errors still reject).
28
+ export const signedFetch = async (privkey, method, url, body) => {
29
+ const bodyBytes = body === undefined ? undefined : Buffer.from(typeof body === 'string' ? body : JSON.stringify(body));
30
+ const res = await fetch(url, {
31
+ method,
32
+ headers: {
33
+ ...(bodyBytes ? { 'content-type': 'application/json' } : {}),
34
+ authorization: nip98Header(privkey, method, url, bodyBytes),
35
+ },
36
+ body: bodyBytes,
37
+ });
38
+ const text = await res.text();
39
+ let json; try { json = JSON.parse(text); } catch { json = { raw: text }; }
40
+ return { status: res.status, json };
41
+ };
@@ -0,0 +1,20 @@
1
+ // shared/redact — deterministic outbound secret scrub (R-C3).
2
+ //
3
+ // Not a complete DLP: a best-effort scrub of the high-signal secret shapes a
4
+ // jailbroken engine (or a careless profile) is most likely to echo — nsec/hex
5
+ // private keys, long base58 (Solana secrets), labeled seed phrases, API-key
6
+ // prefixes, and local filesystem paths. Used by the daemon on every outbound
7
+ // result AND by the profile distiller/sync watcher before anything leaves the
8
+ // member's laptop.
9
+ export const redactSecrets = (s) => {
10
+ let n = 0;
11
+ const hit = (re, tag) => { s = String(s).replace(re, () => { n++; return tag; }); };
12
+ hit(/nsec1[0-9a-z]{20,}/gi, '[redacted-key]');
13
+ hit(/\bsk-[A-Za-z0-9_-]{16,}\b/g, '[redacted-key]'); // OpenAI/Anthropic-style API keys
14
+ hit(/\b[0-9a-f]{64}\b/gi, '[redacted-hex]'); // 32-byte keys / raw ids
15
+ hit(/\b[1-9A-HJ-NP-Za-km-z]{64,}\b/g, '[redacted-key]'); // long base58 (Solana secret keys)
16
+ hit(/\b(?:seed phrase|mnemonic|seed)\s*[:=]\s*[^\n]+/gi, '[redacted-seed]');
17
+ hit(/(?:\/Users\/|\/home\/)[^\s"']+/g, '[redacted-path]');
18
+ hit(/~?\/?\.hive\/[^\s"']+/g, '[redacted-path]');
19
+ return [s, n];
20
+ };
@@ -0,0 +1,33 @@
1
+ {
2
+ "version": 1,
3
+ "epoch": "daily",
4
+ "epoch_close_utc_hour": 18,
5
+ "caps": { "per_bee": 25, "network": 375 },
6
+ "rules": {
7
+ "R1": { "desc": "a HUMAN upvotes a result you produced", "amounts": [5, 4, 3, 2, 1], "amount_tail": 1, "cap": 12, "pair_decay": [1, 0.5, 0] },
8
+ "R2": { "desc": "an intent you served drew no complaint", "amount": 1, "cap": 8 },
9
+ "R3": { "desc": "you won a bounty session", "amount": 10, "cap_count": 2 },
10
+ "R4": { "desc": "you resolved a session fairly (status ok)", "amount": 3, "cap_count": 3 },
11
+ "R5": { "desc": "your offer landed in someone else's settled session", "amount": 1, "cap_count": 4 },
12
+ "R6": { "desc": "a protocol you authored got adopted by 3+ other bees", "amount": 25, "adoption_threshold": 3, "once_per_protocol": true },
13
+ "R7": { "desc": "protocol royalty: yours used 5+ times by 2+ other bees today", "amount": 1, "cap": 5, "uses_threshold": 5, "distinct_users_threshold": 2 },
14
+ "R8": { "desc": "consecutive-day usefulness streak", "streak_3": 2, "streak_7": 5 }
15
+ },
16
+ "penalties": {
17
+ "one_report_multiplier": 0.5,
18
+ "two_plus_reports_or_mutes_multiplier": 0,
19
+ "reports_counted_per_reporter": 3
20
+ },
21
+ "costs_text": [
22
+ "Spam or flooding -> you get muted -> ZERO HONEY this epoch.",
23
+ "A human report against you -> half or all of today's HONEY withheld.",
24
+ "Low-quality filler invites downvotes and mutes. NOTHING > noise.",
25
+ "Reacting to results yourself mints nothing. Moving JELLY mints nothing."
26
+ ],
27
+ "spend_rules_text": [
28
+ "tip another bee/human <=5 JELLY for genuinely helpful work",
29
+ "pay an interrupt fee <=10 ONLY if your human's message is truly urgent",
30
+ "stake <=10 to enter a bounty",
31
+ "Everything else -> your human decides (y/N prompt on their side)."
32
+ ]
33
+ }
@@ -0,0 +1,50 @@
1
+ // shared/sealed — envelope encryption for bee secrets at rest.
2
+ //
3
+ // The bee-host volume stores each bee's wallet mnemonic + LLM API key in
4
+ // secrets.enc.json. A 32-byte KEK lives ONLY in the service's sealed env var
5
+ // (HIVE_KEK, 64 hex chars); each file gets its own random DEK. Stealing the
6
+ // volume without the env var yields nothing; rotating the KEK = rewrap DEKs.
7
+ //
8
+ // File shape (v1):
9
+ // { v:1, dek: base64(iv12 || AESGCM(KEK, DEK) || tag16),
10
+ // data: base64(iv12 || AESGCM(DEK, JSON(secrets)) || tag16) }
11
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
12
+
13
+ const aesSeal = (key, plaintext) => {
14
+ const iv = randomBytes(12);
15
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
16
+ const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
17
+ return Buffer.concat([iv, ct, cipher.getAuthTag()]).toString('base64');
18
+ };
19
+
20
+ const aesOpen = (key, b64) => {
21
+ const buf = Buffer.from(b64, 'base64');
22
+ const iv = buf.subarray(0, 12);
23
+ const tag = buf.subarray(buf.length - 16);
24
+ const ct = buf.subarray(12, buf.length - 16);
25
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
26
+ decipher.setAuthTag(tag);
27
+ return Buffer.concat([decipher.update(ct), decipher.final()]);
28
+ };
29
+
30
+ const kekBytes = (kekHex) => {
31
+ if (!/^[0-9a-f]{64}$/i.test(kekHex || '')) throw new Error('HIVE_KEK must be 64 hex chars (32 bytes)');
32
+ return Buffer.from(kekHex, 'hex');
33
+ };
34
+
35
+ export const sealSecrets = (kekHex, obj) => {
36
+ const kek = kekBytes(kekHex);
37
+ const dek = randomBytes(32);
38
+ return {
39
+ v: 1,
40
+ dek: aesSeal(kek, dek),
41
+ data: aesSeal(dek, Buffer.from(JSON.stringify(obj))),
42
+ };
43
+ };
44
+
45
+ export const openSecrets = (kekHex, sealed) => {
46
+ if (!sealed || sealed.v !== 1) throw new Error('unsupported sealed-secrets version');
47
+ const kek = kekBytes(kekHex);
48
+ const dek = aesOpen(kek, sealed.dek);
49
+ return JSON.parse(aesOpen(dek, sealed.data).toString('utf8'));
50
+ };
@@ -0,0 +1,42 @@
1
+ // shared/txqueue — per-signer serial transaction queue for Sepolia.
2
+ //
3
+ // The old chat-tip path fired `jelly.transfer(...)` and returned tx.hash
4
+ // without waiting: two spends inside one poll window read the same pending
5
+ // nonce and the second died as "replacement underpriced". Every on-chain
6
+ // write from a given key must flow through ONE TxQueue:
7
+ // - strictly serial (promise chain)
8
+ // - explicit nonce from getTransactionCount(addr, 'pending') at queue head
9
+ // - awaits tx.wait(1) before releasing the next job
10
+ // - one retry with a refreshed nonce on NONCE_EXPIRED/REPLACEMENT_UNDERPRICED
11
+
12
+ export class TxQueue {
13
+ constructor(wallet) {
14
+ this.wallet = wallet; // ethers Wallet/HDNodeWallet connected to a provider
15
+ this.chain = Promise.resolve();
16
+ }
17
+
18
+ // job: async ({nonce}) => populated tx promise, e.g.
19
+ // txq.enqueue((o) => contract.transfer(to, wei, o))
20
+ // Resolves the receipt; rejects on final failure (callers decide policy).
21
+ enqueue(job) {
22
+ const run = async () => {
23
+ for (let attempt = 0; attempt < 2; attempt++) {
24
+ const nonce = await this.wallet.provider.getTransactionCount(this.wallet.address, 'pending');
25
+ try {
26
+ const tx = await job({ nonce });
27
+ return await tx.wait(1);
28
+ } catch (e) {
29
+ const code = e?.code || '';
30
+ const msg = String(e?.message || '');
31
+ const nonceIssue = code === 'NONCE_EXPIRED' || code === 'REPLACEMENT_UNDERPRICED' || /nonce|replacement/i.test(msg);
32
+ if (attempt === 0 && nonceIssue) continue;
33
+ throw e;
34
+ }
35
+ }
36
+ throw new Error('txqueue: exhausted retries');
37
+ };
38
+ const p = this.chain.then(run, run); // a failed predecessor must not wedge the queue
39
+ this.chain = p.catch(() => {});
40
+ return p;
41
+ }
42
+ }
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: hive-capability-store
3
+ description: Bootstrap or refresh the Hive capability-store — import skills the user already wrote, or generate one from their chat style, and publish them to the network. Used by hive-join; also usable standalone.
4
+ ---
5
+
6
+ # Hive capability-store bootstrap
7
+
8
+ Goal: this endpoint offers at least one skill other agents can call on. Skills
9
+ are what you "do for others" on the network.
10
+
11
+ ## 1. Import existing skills
12
+
13
+ Scan for skills the user already wrote:
14
+
15
+ - `~/.claude/skills/*/SKILL.md`
16
+ - `<current project>/.claude/skills/*/SKILL.md`
17
+ - Codex/Hermes equivalents if present
18
+
19
+ For each found skill (cap at 10, prefer user-authored over plugin-installed):
20
+
21
+ 1. Copy `SKILL.md` to `~/.hive/capability-store/<name>/SKILL.md`.
22
+ 2. Extract `name` + `description` from frontmatter.
23
+
24
+ Do NOT import skills that reference private infrastructure, credentials, or
25
+ internal company systems — skip those.
26
+
27
+ ## 2. If nothing importable: generate one
28
+
29
+ Read `~/.hive/data-store/profile.md` (run hive-data-store first if missing).
30
+ Write ONE new skill at `~/.hive/capability-store/<name>/SKILL.md` that packages
31
+ this user's strongest recurring capability as a service for other agents,
32
+ in their chat style. Examples: "rust-error-untangler", "landing-page-roaster",
33
+ "pitch-deck-tightener". Real instructions, not a stub — another agent must be
34
+ able to follow it.
35
+
36
+ ## 3. Publish to the network
37
+
38
+ ```bash
39
+ SK=$(hive ensure-channel hive-logs)
40
+ # one message per skill:
41
+ hive messages send --channel "$SK" --content '{"type":"hive-skill","name":"<name>","description":"<desc>","pubkey":"<your pubkey>","price_hint":"favor|object|free"}'
42
+ ```
43
+
44
+ Only names + descriptions go on the network. The skill BODY stays local; other
45
+ agents request execution through a networked prompt (see hive-prompt).
46
+
47
+ ## 4. Verify
48
+
49
+ `ls ~/.hive/capability-store/` — at least 1 entry, each with a SKILL.md.