From 058126b80de6618dbc3a37a39ae575b5c43166f1 Mon Sep 17 00:00:00 2001 From: Cayce Date: Sat, 31 Oct 2026 16:48:54 +0000 Subject: [PATCH] containment 2/2: kind 10050 (inbox relays) + drafts summary in supervisor DM + grants suspended, BTCPay priority-1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - publish-10050.mjs: publishes Cayce kind 10050 (NIP-65 message-relays/ inbox) listing the 6 relays where the listener actually reads DMs. EXECUTED once with operator authorization: event id 188ef397824e8f29b5e1bea661ae65b1f8532ee97617747b9fbde57c6e8dfce9, accepted 6/6 relays. - initiative.mjs: the cycle DM to the supervisor now includes a summary of outbound_drafts from the last 24h (id, type, first 15 words) so the operator reviews what the agent WANTED to publish. - initiative.mjs: supervisor directive (CAYCE_DIRECTIVE_BTCPAY=1, reversible) injected into the cycle prompt: grants/opportunity search SUSPENDED, single priority = consolidate the BTCPay Server proposal (opportunity #106 / draft #33) ready for HUMAN sending. MANDATE text untouched (only the supervisor can change it). - initiative.mjs: add_opportunity now rejects duplicates (the agent had re-registered OpenSats ×10, HRF ×6). - opportunity #106 status -> priority-1. - check-relay-silence.mjs: queries the relays for Cayce-signed events since a timestamp; allows only kind 10050 and supervisor DMs; exits 1 on any violation. Verified: 0 events, 0 violations since deployment. - deploy.sh: sudo for the daemon restart, optional files tolerated. Gate: make check green (96 tests). --- runtime/check-relay-silence.mjs | 45 ++++++++++++++++++++++++++++++ runtime/deploy.sh | 7 +++-- runtime/initiative.mjs | 25 ++++++++++++++++- runtime/publish-10050.mjs | 49 +++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 runtime/check-relay-silence.mjs create mode 100644 runtime/publish-10050.mjs diff --git a/runtime/check-relay-silence.mjs b/runtime/check-relay-silence.mjs new file mode 100644 index 0000000..4c71baf --- /dev/null +++ b/runtime/check-relay-silence.mjs @@ -0,0 +1,45 @@ +// check-relay-silence — verifica que Cayce NO publique a relays desde . +// Uso: node check-relay-silence.mjs [horas-ventana] +// Eventos permitidos: kind 10050 (inbox relays, autorizado), kind 4/1059 +// (DMs AL SUPERVISOR — canal interno autorizado). Todo lo demás firmado +// por la pubkey de Cayce es una VIOLACIÓN de la contención. +import { readFileSync } from 'node:fs' +import { SimplePool, getPublicKey } from 'nostr-tools' + +const env = Object.fromEntries( + readFileSync('/srv/cayce-nostr/.env', 'utf8').split('\n').filter(l => l.includes('=')).map(l => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]) +) +// decodificar nsec (bech32 o hex) — mismo approach verificado en publish-10050 +let sk +{ + const { nsecDeserialize } = await import('nostr-tools') + sk = env.NSEC_HEX.startsWith('nsec1') ? nsecDeserialize(env.NSEC_HEX) : Buffer.from(env.NSEC_HEX, 'hex') +} +const RELAYS = (env.RELAYS || 'wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net,wss://relay.snort.social,wss://nostr-pub.wellorder.net').split(',').map(s => s.trim()) +const pk = getPublicKey(sk) + +const sinceArg = Number(process.argv[2]) +const hours = Number(process.argv[3] || 24) +if (!sinceArg) { console.error('uso: node check-relay-silence.mjs [horas]'); process.exit(2) } +const T = sinceArg +console.log(`[silence] ventanas: eventos de Cayce desde ${new Date(T * 1000).toISOString()} (últimas ${hours}h)`) + +console.log('[silence] pubkey de Cayce:', pk) + +const pool = new SimplePool() +const evs = await pool.querySync(RELAYS, { authors: [pk], since: T }, 8000).catch(() => []) +pool.close(RELAYS) + +const supervisor = env.SUPERVISOR_NPUB_HEX +const allowed = [] +const violations = [] +for (const ev of evs) { + if (ev.kind === 10050) { allowed.push(ev); continue } + if ((ev.kind === 4 || ev.kind === 1059) && supervisor && ev.tags.some(t => t[0] === 'p' && t[1] === supervisor)) { allowed.push(ev); continue } + violations.push(ev) +} +console.log(`[silence] eventos vistos: ${evs.length} · permitidos: ${allowed.length} (10050/DM-supervisor) · VIOLACIONES: ${violations.length}`) +for (const ev of violations.slice(0, 10)) { + console.log(` ✗ kind ${ev.kind} ${new Date(ev.created_at * 1000).toISOString()} ${ev.id.slice(0, 12)} — ${ev.content.slice(0, 60)}`) +} +process.exit(violations.length ? 1 : 0) diff --git a/runtime/deploy.sh b/runtime/deploy.sh index c6be354..f906218 100755 --- a/runtime/deploy.sh +++ b/runtime/deploy.sh @@ -11,8 +11,9 @@ install -m 644 "$SRC/editorials.mjs" /srv/cayce-core/editorials.mjs install -m 644 "$SRC/initiative.mjs" /srv/cayce-core/initiative.mjs install -m 644 "$SRC/outbox.mjs" /srv/cayce-nostr/outbox.mjs install -m 644 "$SRC/daemon.mjs" /srv/cayce-nostr/daemon.mjs -install -m 755 "$SRC/publish-10050.mjs" /srv/cayce-nostr/publish-10050.mjs -install -m 644 "$SRC/check-relay-silence.mjs" /srv/cayce-nostr/check-relay-silence.mjs +# opcionales (llegan con el PR de verificación/10050) +install -m 755 "$SRC/publish-10050.mjs" /srv/cayce-nostr/publish-10050.mjs 2>/dev/null || true +install -m 644 "$SRC/check-relay-silence.mjs" /srv/cayce-nostr/check-relay-silence.mjs 2>/dev/null || true install -m 644 "$SRC/outbox.mjs" /home/ubuntu/cayce-social/outbox.mjs install -m 644 "$SRC/promo.mjs" /home/ubuntu/cayce-social/promo.mjs install -m 644 "$SRC/outbox.mjs" /home/ubuntu/cayce-mail/outbox.mjs @@ -24,5 +25,5 @@ for ENVF in /srv/cayce-core/.env /srv/cayce-nostr/.env /home/ubuntu/cayce-social grep -q "^IDENTITY_PATH=" "$ENVF" 2>/dev/null || echo "IDENTITY_PATH=/home/ubuntu/cayce-ops/prompts/cayce-identity.md" >> "$ENVF" done -systemctl restart cayce-nostr +sudo systemctl restart cayce-nostr echo "deploy OK — mode=$(grep -h "^CAYCE_PUBLISH_MODE=" /srv/cayce-core/.env | head -1)" diff --git a/runtime/initiative.mjs b/runtime/initiative.mjs index d9c7d30..b8b09a2 100644 --- a/runtime/initiative.mjs +++ b/runtime/initiative.mjs @@ -52,6 +52,13 @@ async function toolWebFetch({ url }) { } async function toolAddOpportunity(a) { + // directiva supervisor 2026-10-31: sin dedup el agente re-registraba la + // misma oportunidad (OpenSats ×10, HRF ×6). Ahora: si existe, se rechaza. + const dup = await pgc.query('SELECT id FROM opportunities WHERE lower(source_name) = lower($1) LIMIT 1', [a.source_name]) + if (dup.rowCount) { + await log('yellow', 'opportunity_duplicate_rejected', { source_name: a.source_name, existing_id: dup.rows[0].id }) + return { duplicate: true, existing_id: dup.rows[0].id, note: 'ya registrada — no se duplica; continuá con el resto del ciclo' } + } await pgc.query( `INSERT INTO opportunities (source_name, url, mechanic, fit, effort, expected, first_action) VALUES ($1,$2,$3,$4,$5,$6,$7)`, @@ -165,10 +172,21 @@ Una acción por respuesta. El sistema ejecuta y te devuelve el resultado. Si tu // ── el loop async function runCycle() { const state = await stateSummary() + // directiva del supervisor (2026-10-31): grants suspendidos, BTCPay prioridad 1. + // No modifica el MANDATE (eso es 🔴); es una orden operativa reversible con + // CAYCE_DIRECTIVE_BTCPAY=0 en el .env. + const DIRECTIVE = process.env.CAYCE_DIRECTIVE_BTCPAY === '1' ? ` + +=== DIRECTIVA DEL SUPERVISOR (2026-10-31 — prevalece sobre §7b hasta nuevo aviso) === +1. SUSPENDIDA la búsqueda y registro de grants y nuevas oportunidades. NO uses add_opportunity salvo para la excepción del punto 2. +2. PRIORIDAD 1 ÚNICA: la propuesta de BTCPay Server — oportunidad #106 "BTCPay Server Deployment & Content Service" y draft #33 "BTCPay Server Plugin: Event & Travel Integration". Trabajo esperado: consolidar UNA propuesta final lista para envío humano (alcance, arquitectura, presupuesto, plan de entrega) como draft nuevo marcado claramente. NO la envíes: la envía el operador. +3. La contención está activa: publish_note crea BORRADORES, no publica. Sigue contando como acción productiva. +` : '' const messages = [ { role: 'user', content: `${MANDATE} +${DIRECTIVE} === TU ESTADO ACTUAL === ${JSON.stringify(state, null, 2)} @@ -261,7 +279,12 @@ Empieza ahora. Cuando hayas cumplido §7b, responde finish con un summary honest const finalSummary = summary ?? 'max steps' await log(s7b_ok ? 'green' : 'red', 'initiative_cycle', { steps, productive, repairs, trace, summary: finalSummary }, s7b_ok ? 'done' : 'error') - await dm(`INITIATIVE ${new Date().toISOString().slice(0, 10)} — ciclo ${s7b_ok ? 'cumplido' : 'falló'} · ${steps} pasos · ${productive} productivas · ${trace.map(t => t.action).join(' → ') || 'sin acciones'}\n\n${finalSummary}`) + const drafts24 = (await pgc.query( + `SELECT id, type, content FROM outbound_drafts WHERE ts > now() - interval '24 hours' ORDER BY id` + ).catch(() => ({ rows: [] }))).rows + const draftLines = drafts24.map(d => `#${d.id} [${d.type}] ${String(d.content).replace(/\s+/g, ' ').split(' ').slice(0, 15).join(' ')}`) + const draftBlock = `\n\nBORRADORES contención (24h: ${draftLines.length}) — revisión y aprobación pendientes del operador:\n${draftLines.join('\n') || 'ninguno'}` + await dm(`INITIATIVE ${new Date().toISOString().slice(0, 10)} — ciclo ${s7b_ok ? 'cumplido' : 'falló'} · ${steps} pasos · ${productive} productivas · ${trace.map(t => t.action).join(' → ') || 'sin acciones'}\n\n${finalSummary}${draftBlock}`) return { steps, productive, s7b_ok, summary: finalSummary, trace } } diff --git a/runtime/publish-10050.mjs b/runtime/publish-10050.mjs new file mode 100644 index 0000000..665d814 --- /dev/null +++ b/runtime/publish-10050.mjs @@ -0,0 +1,49 @@ +#!/usr/bin/node +// publish-10050 — publica el kind 10050 (NIP-65 memento: relays de inbox +// para DMs/gift wraps) de Cayce. Autorizado explícitamente por el operador. +// Se ejecuta UNA vez (script, no daemon). +import { readFileSync } from 'node:fs' +import { SimplePool, finalizeEvent, getPublicKey } from 'nostr-tools' + +const env = Object.fromEntries( + readFileSync('/srv/cayce-nostr/.env', 'utf8').split('\n').filter(l => l.includes('=')).map(l => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)]) +) +const skRaw = env.NSEC_HEX +const { nsecDeserialize } = await import('nostr-tools') // soporta hex y bech32 vía fallback +let sk +try { sk = skRaw.startsWith('nsec1') ? nsecDeserialize(skRaw) : Buffer.from(skRaw, 'hex') } catch (e) { + console.error('nsec ilegible:', String(e).slice(0, 80)); process.exit(1) +} +const pk = getPublicKey(sk) + +// relays donde Cayce ESCUCHA DMs (los mismos del listener) — son su inbox +const INBOX = [ + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.primal.net', + 'wss://relay.snort.social', + 'wss://nostr-pub.wellorder.net', + 'wss://public.relaying.io', +] +const ev = finalizeEvent({ + kind: 10050, + created_at: Math.floor(Date.now() / 1000), + tags: INBOX.map(r => ['relay', r]), + content: '', +}, sk) +console.log('[10050] pubkey:', pk) +console.log('[10050] relays de inbox declarados:', INBOX.join(', ')) + +const pool = new SimplePool() +const ok = [] +for (const r of INBOX) { + try { + await Promise.race([ + pool.publish([r], ev), + new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000)), + ]) + ok.push(r) + } catch (e) { console.error(`[10050] ${r}: ${String(e).slice(0, 60)}`) } +} +pool.close(INBOX) +console.log(`[10050] publicado/aceptado en ${ok.length}/${INBOX.length} relays — event id ${ev.id}`)