From 571d0a62ff562f72e81b723cb1db2f857ec6dfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 13 Aug 2026 00:23:32 +0000 Subject: [PATCH] delete the bug-report discord webhook, and the last dead HOME_DIR reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DISCORD_BUG_REPORT_WEBHOOK is gone, with sendToDiscord and its helpers. Reports still land in DATA_PATH/bug-reports — the disk write always happened first and the webhook was only a ping about it, so nothing about the report is lost. It was a personal notification channel living in deployment config, on a platform whose owner is the only person who files reports. It was also never in .env.example: the setup script wrote a variable nothing documented, which is the same drift as PORT, in the other direction. Note DISCORD_WEBHOOK_URL is a DIFFERENT variable — the notify sidecar's own channel — and is untouched. Then a parity sweep of setup / .env.example / what the code reads, which turned up two leftovers from earlier today: HOME_DIR was still read in six files, each with its own `?? homedir()` fallback. Dead since nothing sets it, but a dead read is worse than none — it reads as a supported override. They take homedir() directly now. user-instance.ts gets a comment on why its line stays where it is: it sits above `process.env.HOME = homeDir`, and homedir() reads $HOME, so a read moved below that assignment would return whichever member was last spawned into. Two of the six had fallback chains ending in process.cwd() and '' — the second would have silently disabled whatever consumed it rather than failing. VAULTWARDEN_URL was uncommented in .env.example among the variables setup writes, though it is a plugin variable setup has never written. Commented out with the other plugin entries. The three files now agree: setup writes PORT, BROWSER_RELAY_PORT and POSTGRES_URL; .env.example lists those plus JWT_SECRET and VAULT_STORE_KEY, which are required by code and deliberately unwritten until the secret store lands. Not typechecked (empty node_modules, frozen installs). Every changed file parses; the setup section was run and writes three variables. Co-Authored-By: Claude Opus 5 (1M context) --- .env.example | 2 +- scripts/setup/officer-setup.sh | 1 - scripts/setup/officer-setup/lib/env.sh | 1 - src/servers/api/activity/router.ts | 3 +- src/servers/api/bug-report/bug-report.ts | 76 +++------------------ src/servers/api/chat/claude-sessions.ts | 4 +- src/servers/sidecar/claude/user-instance.ts | 4 +- src/servers/sidecar/music/cliamp-ws.ts | 2 +- src/servers/sidecar/music/indexer.ts | 2 +- src/servers/sidecar/music/stream-audio.ts | 2 +- src/servers/sidecar/pty/sessions.mjs | 3 +- 11 files changed, 23 insertions(+), 77 deletions(-) diff --git a/.env.example b/.env.example index 65cffff3..3257fb9f 100644 --- a/.env.example +++ b/.env.example @@ -54,7 +54,7 @@ VAULT_STORE_KEY="" # Vaultwarden (officer-vault). VAULT_STORE_KEY is at the top of this file — it is the platform's # key, not Vaultwarden's, however much the name and its old position here suggested otherwise. -VAULTWARDEN_URL=http://127.0.0.1:8222 +# VAULTWARDEN_URL=http://127.0.0.1:8222 # The Anthropic proxy (officer-anthropic-proxy) binds PORT + 1, derived rather than configured — see # src/servers/officer-url.mjs. There is nothing to set. It holds no credential from this file either: diff --git a/scripts/setup/officer-setup.sh b/scripts/setup/officer-setup.sh index e3939eb9..905c2563 100755 --- a/scripts/setup/officer-setup.sh +++ b/scripts/setup/officer-setup.sh @@ -460,7 +460,6 @@ if ! skip; then # Read back before anything is asked; existing values become the defaults. ENV_PORT="$(env_get PORT)" - ENV_DISCORD_WEBHOOK="$(env_get DISCORD_BUG_REPORT_WEBHOOK)" ENV_BROWSER_RELAY_PORT="$(env_get BROWSER_RELAY_PORT)" if env_exists; then diff --git a/scripts/setup/officer-setup/lib/env.sh b/scripts/setup/officer-setup/lib/env.sh index 66309808..077b6d38 100644 --- a/scripts/setup/officer-setup/lib/env.sh +++ b/scripts/setup/officer-setup/lib/env.sh @@ -70,7 +70,6 @@ BROWSER_RELAY_PORT="${ENV_BROWSER_RELAY_PORT}" POSTGRES_URL="${POSTGRES_URL}" -DISCORD_BUG_REPORT_WEBHOOK="${ENV_DISCORD_WEBHOOK}" ENVF umask "$prior_umask" diff --git a/src/servers/api/activity/router.ts b/src/servers/api/activity/router.ts index e77902e4..691ff2ef 100644 --- a/src/servers/api/activity/router.ts +++ b/src/servers/api/activity/router.ts @@ -13,8 +13,9 @@ import { parseTailLine } from './progress'; export const activityRouter = createRouter(); import { DATA_PATH } from '../../data-path'; +import { homedir } from 'node:os'; -const HOME_DIR = process.env.HOME_DIR ?? process.env.HOME ?? ''; +const HOME_DIR = homedir(); const ANNOUNCED_PATH = join(DATA_PATH, 'activity', 'announced.json'); const ALLOWED_ROOTS = ['/tmp', DATA_PATH, HOME_DIR].filter(Boolean); const ACTIVE_WINDOW_MS = 120_000; // a task file touched within this is considered "active" diff --git a/src/servers/api/bug-report/bug-report.ts b/src/servers/api/bug-report/bug-report.ts index a47ad3a2..bab414f4 100644 --- a/src/servers/api/bug-report/bug-report.ts +++ b/src/servers/api/bug-report/bug-report.ts @@ -3,7 +3,15 @@ import { mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { DATA_PATH } from '@@/data-path'; -const DISCORD_WEBHOOK_URL = process.env.DISCORD_BUG_REPORT_WEBHOOK; +// Bug reports land on disk and nowhere else. +// +// There was a Discord webhook here until 2026-08-13, behind DISCORD_BUG_REPORT_WEBHOOK. It was a +// personal notification channel living in deployment config, on a self-hosted platform whose owner is +// the only person filing reports — and it was never in .env.example, so the setup script wrote a +// variable nothing documented. +// +// Nothing is lost from the report itself: the disk write below always happened first, and the webhook +// was only a ping about it. export const bugReportRouter = createRouter(); @@ -36,73 +44,9 @@ bugReportRouter.post('/', async (ctx) => { await Bun.write(join(reportDir, 'report.json'), JSON.stringify(report, null, 2)); - let screenshotBuffer: Buffer | null = null; if (screenshot instanceof File) { - screenshotBuffer = Buffer.from(await screenshot.arrayBuffer()); - await Bun.write(join(reportDir, 'screenshot.png'), screenshotBuffer); - } - - if (DISCORD_WEBHOOK_URL) { - await sendToDiscord(report, screenshotBuffer); + await Bun.write(join(reportDir, 'screenshot.png'), Buffer.from(await screenshot.arrayBuffer())); } return ctx.json({ ok: true, id: dirName }); }); - -type BugReport = { - description: string; - context: { - url?: string; - userAgent?: string; - viewport?: { width: number; height: number }; - apiError?: { status: number; message: string } | null; - } | null; - reporter: { id: number; email: string; name: string | null }; - createdAt: string; -}; - -async function sendToDiscord(report: BugReport, screenshot: Buffer | null) { - const embed = { - title: 'Bug Report', - description: report.description, - color: 0xed4245, - fields: [ - { name: 'Reporter', value: `${report.reporter.name} (${report.reporter.email})`, inline: true }, - { name: 'URL', value: report.context?.url ?? 'N/A', inline: false }, - { - name: 'Viewport', - value: report.context?.viewport ? `${report.context.viewport.width}x${report.context.viewport.height}` : 'N/A', - inline: true, - }, - { name: 'Browser', value: shortenUA(report.context?.userAgent), inline: true }, - ], - timestamp: report.createdAt, - }; - - if (report.context?.apiError) { - embed.fields.push({ - name: 'Last API Error', - value: `${report.context.apiError.status}: ${report.context.apiError.message}`, - inline: false, - }); - } - - const form = new FormData(); - form.append('payload_json', JSON.stringify({ embeds: [embed] })); - - if (screenshot) { - form.append('files[0]', new Blob([new Uint8Array(screenshot)], { type: 'image/png' }), 'screenshot.png'); - } - - const res = await fetch(DISCORD_WEBHOOK_URL!, { method: 'POST', body: form }); - if (!res.ok) { - console.error('[bug-report] Discord webhook failed:', res.status, await res.text()); - } -} - -function shortenUA(ua?: string): string { - if (!ua) return 'N/A'; - const browser = ua.match(/(Chrome|Firefox|Safari|Edge|Brave|OPR)\/[\d.]+/)?.[0] ?? ''; - const os = ua.match(/\(([^)]+)\)/)?.[1]?.split(';')[0] ?? ''; - return [browser, os].filter(Boolean).join(' — ') || ua.slice(0, 80); -} diff --git a/src/servers/api/chat/claude-sessions.ts b/src/servers/api/chat/claude-sessions.ts index 0755dffc..b26d5222 100644 --- a/src/servers/api/chat/claude-sessions.ts +++ b/src/servers/api/chat/claude-sessions.ts @@ -23,8 +23,8 @@ import { DATA_PATH } from '../../data-path'; // // ── Why this takes a home instead of an email ── // -// It used to be `process.env.HOME_DIR ?? join(DATA_PATH, email, 'home')`, which discards its argument whenever -// HOME_DIR is set — which is always, on a real install. Every read therefore resolved to the OWNER'S +// It used to be `process.env.HOME_DIR ?? join(DATA_PATH, email, 'home')`, which discarded its argument +// whenever HOME_DIR was set — which was always, on a real install. Every read therefore resolved to the OWNER'S // transcripts regardless of who was asking, and the comment above it said "single-user platform" as though // that were a property rather than an assumption. A member reaching these functions would have been handed the // owner's conversation list. diff --git a/src/servers/sidecar/claude/user-instance.ts b/src/servers/sidecar/claude/user-instance.ts index 6daeec9f..cf7abb46 100644 --- a/src/servers/sidecar/claude/user-instance.ts +++ b/src/servers/sidecar/claude/user-instance.ts @@ -80,7 +80,9 @@ const OFFICER_AUTH_TOKEN = await sign({ id: dbUser.id, email, username: dbUser.u // perfect parity with terminal sessions (same config, credentials and transcript store, // interchangeable via `claude --resume`). That absence of isolation is precisely why `chat` is an // `execution` capability and can never be granted: this is a shell, not a feature flag. -const homeDir = process.env.HOME_DIR ?? homedir(); +// Evaluated here, ABOVE the `process.env.HOME = homeDir` below: homedir() reads $HOME, so a +// read placed after that assignment would return whatever was last spawned into. +const homeDir = homedir(); const globalToolsDir = join(DATA_PATH, 'tools'); // The email_db MCP tool reads one account's SQLite store; match the API's choice (first enabled). diff --git a/src/servers/sidecar/music/cliamp-ws.ts b/src/servers/sidecar/music/cliamp-ws.ts index 7249d166..d3bb564c 100644 --- a/src/servers/sidecar/music/cliamp-ws.ts +++ b/src/servers/sidecar/music/cliamp-ws.ts @@ -20,7 +20,7 @@ const ASOUNDRC_PATH = join(import.meta.dir, 'asoundrc'); // Single super user, so the owner's home is the root every path is resolved against — same convention as // stream-audio.ts and the indexer. -const ROOT_DIR = process.env.HOME_DIR ?? homedir(); +const ROOT_DIR = homedir(); // parec's output format IS the contract with the browser's AudioWorklet: signed 16-bit LE, 44.1kHz, stereo. const CAPTURE_ARGS = ['--format=s16le', '--rate=44100', '--channels=2', '-d', `${VIRTUAL_SINK}.monitor`]; diff --git a/src/servers/sidecar/music/indexer.ts b/src/servers/sidecar/music/indexer.ts index 3dd92468..3f5431c6 100644 --- a/src/servers/sidecar/music/indexer.ts +++ b/src/servers/sidecar/music/indexer.ts @@ -22,7 +22,7 @@ import { homedir } from 'node:os'; import { DATA_PATH } from '../../data-path'; -const HOME = process.env.HOME_DIR ?? homedir(); +const HOME = homedir(); export const MUSIC_ROOT = join(HOME, 'Music'); const CACHE_ROOT = join(DATA_PATH, 'music', 'cache'); const MANIFEST_PATH = join(CACHE_ROOT, 'manifest.json'); diff --git a/src/servers/sidecar/music/stream-audio.ts b/src/servers/sidecar/music/stream-audio.ts index f0eeaa8d..52ebc734 100644 --- a/src/servers/sidecar/music/stream-audio.ts +++ b/src/servers/sidecar/music/stream-audio.ts @@ -5,7 +5,7 @@ import { homedir } from 'node:os'; // All processing lives here (the platform is just a proxy). Files live under the owner's home — single // super user, so HOME_DIR. Paths from the app are home-relative (e.g. "Music/Artist/Album/track.mp3"), // exactly like file-browser /raw. -const ROOT_DIR = process.env.HOME_DIR ?? homedir(); +const ROOT_DIR = homedir(); const CONTENT_TYPES: Record = { mp3: 'audio/mpeg', diff --git a/src/servers/sidecar/pty/sessions.mjs b/src/servers/sidecar/pty/sessions.mjs index b6ce3747..01585b4c 100644 --- a/src/servers/sidecar/pty/sessions.mjs +++ b/src/servers/sidecar/pty/sessions.mjs @@ -1,5 +1,6 @@ import { join } from 'node:path'; import * as pty from 'node-pty'; +import { homedir } from 'node:os'; // The shell store. Everything about running a terminal lives here: what shell, where it opens, how much // scrollback is kept, who is watching. The platform holds none of it — it authenticates a browser and @@ -11,7 +12,7 @@ const BUFFER_MAX = 512 * 1024; // HOME_DIR mirrors `data-path.ts:getOwnerHomeDir` — on a host where the owner's real login home differs // from this process's HOME, the shell should open in the former, like every other host-executing surface. -const HOME_DIR = process.env.HOME_DIR ?? process.env.HOME ?? process.cwd(); +const HOME_DIR = homedir(); const SHELL = { command: process.env.SHELL ?? '/bin/zsh', args: ['-i'] }; // A terminal is always a plain host shell: the owner is the only account and it is their own machine