← back to George Gmail
George external-send guard: block outbound sends to non-Steve recipients unless human-approval token present (X-Send-Approval / send_approval_token); fail-closed. Born from autonomous-worker auto-send incident 2026-06-17. NOTE: must also deploy to the Kamatera george (the instance MCP actually uses).
a42bc74cd3bc79bcf20d6ca16edfc06e596fe918 · 2026-06-17 15:02:25 -0700 · Steve Abrams
Files touched
Diff
commit a42bc74cd3bc79bcf20d6ca16edfc06e596fe918
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 17 15:02:25 2026 -0700
George external-send guard: block outbound sends to non-Steve recipients unless human-approval token present (X-Send-Approval / send_approval_token); fail-closed. Born from autonomous-worker auto-send incident 2026-06-17. NOTE: must also deploy to the Kamatera george (the instance MCP actually uses).
---
server.js | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/server.js b/server.js
index 51515ab..281d6ea 100644
--- a/server.js
+++ b/server.js
@@ -1059,10 +1059,36 @@ async function resolveReplyContext(gmail, replyToMessageId) {
};
}
+// ── External-send guard (2026-06-17) ─────────────────────────────────────────
+// Incident: an autonomous worker drained gated vendor-email memos and auto-sent
+// them via George. Fix at the chokepoint — sends to Steve-owned addresses are
+// always allowed; any send to an EXTERNAL recipient requires a human-approval
+// token (header X-Send-Approval, or body.send_approval_token). Fail-closed.
+const EXT_SEND_TOKEN = (() => {
+ if (process.env.GEORGE_EXTERNAL_SEND_TOKEN) return process.env.GEORGE_EXTERNAL_SEND_TOKEN;
+ try { const m = require('fs').readFileSync(require('path').join(__dirname, '.env'), 'utf8').match(/^GEORGE_EXTERNAL_SEND_TOKEN=(.+)$/m); return m ? m[1].trim() : ''; } catch (e) { return ''; }
+})();
+const INTERNAL_DOMAINS = (process.env.GEORGE_INTERNAL_DOMAINS || 'designerwallcoverings.com').toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
+const INTERNAL_ADDRS = (process.env.GEORGE_INTERNAL_ADDRS || 'steveabramsdesigns@gmail.com,theagentabrams@gmail.com,wallsandfabrics@gmail.com').toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
+function _recips(...vals) { const out = []; for (const v of vals) { if (!v) continue; const arr = Array.isArray(v) ? v : String(v).split(','); for (const a of arr) { const m = String(a).match(/[\w.+-]+@[\w.-]+\.\w+/); if (m) out.push(m[0].toLowerCase()); } } return out; }
+function _isInternal(addr) { const dom = (addr.split('@')[1] || ''); return INTERNAL_ADDRS.includes(addr) || INTERNAL_DOMAINS.includes(dom); }
+function externalSendGuard(req) {
+ const b = req.body || {};
+ const external = _recips(b.to, b.cc, b.bcc).filter(a => !_isInternal(a));
+ if (!external.length) return { ok: true };
+ const got = (req.get && req.get('x-send-approval')) || b.send_approval_token || '';
+ if (EXT_SEND_TOKEN && got && got === EXT_SEND_TOKEN) return { ok: true, external };
+ return { ok: false, external, reason: (EXT_SEND_TOKEN
+ ? 'external send blocked — missing/invalid human-approval token (pass X-Send-Approval). '
+ : 'external send blocked — GEORGE_EXTERNAL_SEND_TOKEN not configured (fail-closed). ')
+ + 'External recipients: ' + external.join(', ') };
+}
+
app.post('/api/send', async (req, res) => {
try {
const { from, to, subject, body, cc, bcc, threadId: bodyThreadId, inReplyTo: bodyInReplyTo, replyToMessageId, references } = req.body;
if (!to || !body) return res.status(400).json({ error: 'to and body required' });
+ { const _g = externalSendGuard(req); if (!_g.ok) { try { audit(req.body.account || '?', 'send-BLOCKED', { to, external: _g.external, reason: _g.reason }); } catch (e) {} return res.status(403).json({ error: _g.reason, blocked: true, external: _g.external }); } }
const { gmail: g, key: account } = resolveAccount(req);
if (!g) return res.status(400).json({ error: `unknown account: ${account}` });
@@ -1226,6 +1252,7 @@ app.post('/api/send-with-attachment', async (req, res) => {
if (!attachments || !Array.isArray(attachments) || attachments.length === 0) {
return res.status(400).json({ error: 'attachments array required' });
}
+ { const _g = externalSendGuard(req); if (!_g.ok) { try { audit(account || '?', 'send-BLOCKED', { to, external: _g.external, reason: _g.reason }); } catch (e) {} return res.status(403).json({ error: _g.reason, blocked: true, external: _g.external }); } }
const useInfo = account === 'info';
const authClient = useInfo ? infoOauth2Client : oauth2Client;
const gmailClient = useInfo ? infoGmail : gmail;
← 918647c Add launchd KeepAlive supervisor for george-gmail :9850 (dur
·
back to George Gmail
·
Add DELETE /api/drafts/:id route + make GET /api/drafts acco 2626a50 →