Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87a2829b8a | ||
|
|
f618574043 | ||
|
|
058126b80d | ||
|
|
e06cb85ce8 |
@@ -0,0 +1,45 @@
|
||||
// check-relay-silence — verifica que Cayce NO publique a relays desde <T>.
|
||||
// Uso: node check-relay-silence.mjs <epoch-segundos-desde> [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 <epoch_s> [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)
|
||||
+4
-3
@@ -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)"
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { Router } from 'express'
|
||||
import { prisma } from '../lib/prisma'
|
||||
import { ingestAllSources } from '../services/ingestion'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// ── Articles ──────────────────────────────────────────────
|
||||
|
||||
// GET /admin/articles?status=QUEUED&topic=lightning
|
||||
router.get('/articles', async (req, res) => {
|
||||
const { status, topic } = req.query
|
||||
|
||||
const articles = await prisma.article.findMany({
|
||||
where: {
|
||||
...(status ? { status: status as any } : {}),
|
||||
...(topic ? { topics: { has: topic as string } } : {}),
|
||||
},
|
||||
include: { source: { select: { name: true } } },
|
||||
orderBy: { publishedAt: 'desc' },
|
||||
take: 50,
|
||||
})
|
||||
|
||||
res.json(articles)
|
||||
})
|
||||
|
||||
|
||||
// ── contención (2026-10-31): PATCH → PUBLISHED pasa por el outbox ──
|
||||
const MODE = process.env.HARDCAP_PUBLISH_MODE || 'draft';
|
||||
let pgCache = null;
|
||||
function caycePool() {
|
||||
if (!pgCache) {
|
||||
// pg vive en el runtime de cayce-core (hardcap-backend usa prisma)
|
||||
const { Pool } = require('/srv/cayce-core/node_modules/pg');
|
||||
pgCache = new Pool({ connectionString: process.env.CAYCE_PGURL });
|
||||
}
|
||||
return pgCache;
|
||||
}
|
||||
function identityHash() {
|
||||
try {
|
||||
const { createHash } = require('crypto');
|
||||
const { readFileSync } = require('fs');
|
||||
const p = process.env.IDENTITY_PATH || '/home/ubuntu/cayce-ops/prompts/cayce-identity.md';
|
||||
return createHash('sha256').update(readFileSync(p)).digest('hex');
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
// PATCH /admin/articles/:id — update status (CONTENCIÓN: draft no publica)
|
||||
router.patch('/articles/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { status } = req.body
|
||||
if (status === 'PUBLISHED' && MODE !== 'live') {
|
||||
try {
|
||||
const art = await prisma.article.findUnique({ where: { id } })
|
||||
const content = `${art?.title || id} | ${art?.summary || ''}`.slice(0, 400)
|
||||
const ih = identityHash()
|
||||
const r = await caycePool().query(
|
||||
"INSERT INTO outbound_drafts (channel, type, content, identity_path, identity_hash, meta) VALUES ($1,$2,$3,$4,$5,$6) RETURNING id",
|
||||
['hardcap', 'article_publish', content,
|
||||
process.env.IDENTITY_PATH || '/home/ubuntu/cayce-ops/prompts/cayce-identity.md',
|
||||
ih, JSON.stringify({ article_id: id })]
|
||||
)
|
||||
console.log(`[outbox] STAGED draft #${r.rows[0].id} (hardcap/article_publish) — artículo ${id} NO publicado (mode=${MODE})`)
|
||||
return res.json({ staged: true, draft_id: String(r.rows[0].id), article_id: id })
|
||||
} catch (e) {
|
||||
// fail-closed: sin borrador, NO se publica. Sin volcar el comando ni el DSN al log.
|
||||
console.error('[outbox] staging failed — PATCH descartado (fail-closed):', String((e && e.message) || e).slice(0, 120))
|
||||
return res.status(500).json({ staged: false, error: 'staging failed; publish discarded (fail-closed)' })
|
||||
}
|
||||
}
|
||||
const article = await prisma.article.update({
|
||||
where: { id },
|
||||
data: { status },
|
||||
})
|
||||
|
||||
res.json(article)
|
||||
})
|
||||
|
||||
// ── Sources ──────────────────────────────────────────────
|
||||
|
||||
// GET /admin/sources
|
||||
router.get('/sources', async (_req, res) => {
|
||||
const sources = await prisma.source.findMany({ orderBy: { name: 'asc' } })
|
||||
res.json(sources)
|
||||
})
|
||||
|
||||
// POST /admin/sources
|
||||
router.post('/sources', async (req, res) => {
|
||||
const { name, rssUrl, tier, topicBias } = req.body
|
||||
const source = await prisma.source.create({
|
||||
data: { name, rssUrl, tier: tier ?? 2, topicBias: topicBias ?? [] },
|
||||
})
|
||||
res.json(source)
|
||||
})
|
||||
|
||||
// PATCH /admin/sources/:id — toggle active, update tier
|
||||
router.patch('/sources/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { active, tier, topicBias } = req.body
|
||||
|
||||
const source = await prisma.source.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(active !== undefined ? { active } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(topicBias !== undefined ? { topicBias } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
res.json(source)
|
||||
})
|
||||
|
||||
// ── Topic Filters ─────────────────────────────────────────
|
||||
|
||||
// GET /admin/topics
|
||||
router.get('/topics', async (_req, res) => {
|
||||
const topics = await prisma.topicFilter.findMany({ orderBy: { label: 'asc' } })
|
||||
res.json(topics)
|
||||
})
|
||||
|
||||
// PATCH /admin/topics/:id — toggle active, adjust weight
|
||||
router.patch('/topics/:id', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { active, weight, keywordsInclude, keywordsExclude } = req.body
|
||||
|
||||
const topic = await prisma.topicFilter.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(active !== undefined ? { active } : {}),
|
||||
...(weight !== undefined ? { weight } : {}),
|
||||
...(keywordsInclude !== undefined ? { keywordsInclude } : {}),
|
||||
...(keywordsExclude !== undefined ? { keywordsExclude } : {}),
|
||||
},
|
||||
})
|
||||
|
||||
res.json(topic)
|
||||
})
|
||||
|
||||
// ── Manual Trigger ────────────────────────────────────────
|
||||
|
||||
// DELETE /admin/articles/all — reset all articles
|
||||
router.delete('/articles/all', async (_req, res) => {
|
||||
const result = await prisma.article.deleteMany({})
|
||||
res.json({ deleted: result.count })
|
||||
})
|
||||
|
||||
// POST /admin/ingest — trigger ingestion manually
|
||||
router.post('/ingest', async (_req, res) => {
|
||||
res.json({ message: 'Ingestion started' })
|
||||
// Fire and forget
|
||||
ingestAllSources().catch(console.error)
|
||||
})
|
||||
|
||||
export default router
|
||||
+24
-1
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -13,6 +13,9 @@
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
const require_ = createRequire(import.meta.url)
|
||||
|
||||
const IDENTITY_PATH = process.env.IDENTITY_PATH || '/home/ubuntu/cayce-ops/prompts/cayce-identity.md'
|
||||
|
||||
@@ -67,7 +70,7 @@ let ownPool = null
|
||||
async function poolFor(poolPg) {
|
||||
if (poolPg) return poolPg
|
||||
if (!ownPool) {
|
||||
const pg = (await import('pg')).default
|
||||
const pg = require_('/srv/cayce-core/node_modules/pg')
|
||||
const env = Object.fromEntries(
|
||||
readFileSync('/srv/cayce-core/.env', 'utf8').split('\n').filter(l => l.includes('=')).map(l => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)])
|
||||
)
|
||||
|
||||
@@ -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}`)
|
||||
Reference in New Issue
Block a user