runtime: outbox UNICO (imports absolutos) + relay-watcher con kill switch + guard R1-R8
ci / lint-test-leaks (pull_request) Failing after 3s
ci / lint-test-leaks (pull_request) Failing after 3s
- outbox.mjs solo en /srv/cayce-core; daemon/social/mail/ops lo importan por ruta absoluta. deploy.sh ya no instala copias. - relay-watcher.mjs: cada 5 min cruza eventos de la pubkey en 5 relays con outbound_drafts; sin aprobacion => PUBLISH_OFF + DM + audit. Probado: detecto 2 kind0 bypass reales (post-hoc registrados). - guard R1-R8 (18 tests) + blocklist + check-single-outbox al check diario. - stale duplicados archivados en forensics/scripts-archive-20261102 (nostr-daemon.mjs, dm-debug, publish-10050, dm-test2, find-followers).
This commit is contained in:
+27
-5
@@ -11,7 +11,8 @@
|
||||
//
|
||||
// Reversibilidad: una línea por servicio en su .env (CAYCE_PUBLISH_MODE=live).
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { guardCheck, logRejection, recordPublish, ensureGuardTables, FORCE_DRAFT_CHANNELS } from './guard.mjs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
@@ -20,6 +21,8 @@ const require_ = createRequire(import.meta.url)
|
||||
const IDENTITY_PATH = process.env.IDENTITY_PATH || '/home/ubuntu/cayce-ops/prompts/cayce-identity.md'
|
||||
|
||||
export function publishMode() {
|
||||
// kill switch instantáneo (2026-11-01): touch /srv/cayce-core/PUBLISH_OFF
|
||||
if (existsSync('/srv/cayce-core/PUBLISH_OFF')) return 'draft'
|
||||
return (process.env.CAYCE_PUBLISH_MODE || 'draft').toLowerCase()
|
||||
}
|
||||
|
||||
@@ -93,15 +96,34 @@ export async function stageDraft(poolPg, { channel, type, content, original_even
|
||||
}
|
||||
|
||||
// El único punto por el que debe pasar cualquier salida a terceros.
|
||||
// publishFn SOLO se invoca en modo live.
|
||||
// publish-guard (2026-11-01): TODA salida pasa por guardCheck() ANTES de
|
||||
// publicarse o guardarse. Rechazos → tabla guard_rejections (revisable).
|
||||
// mail/proposal/dm SIEMPRE borrador (política del propietario).
|
||||
export async function gateOutbound(poolPg, { channel, type, content, original_event = null, meta = {}, publishFn }) {
|
||||
const p = await poolFor(poolPg)
|
||||
await ensureGuardTables(p)
|
||||
if (FORCE_DRAFT_CHANNELS.includes(channel)) {
|
||||
try {
|
||||
return await stageDraft(p, { channel, type, content, original_event, meta })
|
||||
} catch (e) {
|
||||
console.error(`[outbox] FATAL staging canal-forzado (${String(e).slice(0, 160)}) — descartado`)
|
||||
return { staged: false, error: 'staging failed; discarded (fail-closed)' }
|
||||
}
|
||||
}
|
||||
const v = await guardCheck(p, { channel, type, content })
|
||||
if (!v.ok) {
|
||||
await logRejection(p, { channel, type, content, reasons: v.reasons })
|
||||
return { staged: false, rejected: true, reasons: v.reasons }
|
||||
}
|
||||
if (publishMode() === 'live') {
|
||||
return { ...(await publishFn()), staged: false }
|
||||
const r = await publishFn()
|
||||
// watcher: el id del evento publicado queda registrado como aprobacion
|
||||
await recordPublish(p, { channel, type, content, meta: { ...meta, event_id: r && r.id ? r.id : undefined } })
|
||||
return { ...r, staged: false, guarded: true }
|
||||
}
|
||||
try {
|
||||
return await stageDraft(poolPg, { channel, type, content, original_event, meta })
|
||||
return await stageDraft(p, { channel, type, content, original_event, meta })
|
||||
} catch (e) {
|
||||
// fail-closed: sin borrador guardado, no se publica
|
||||
console.error(`[outbox] FATAL: staging failed (${String(e).slice(0, 160)}) — evento DESCARTADO, no publicado`)
|
||||
return { staged: false, error: 'staging failed; discarded (fail-closed)' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user