diff --git a/README.md b/README.md index f00dd02..7743cfe 100644 --- a/README.md +++ b/README.md @@ -749,6 +749,17 @@ pairing: | `/pair list` | List pending codes and approved senders | | `/pair revoke ` | Revoke an approved sender | +## WhatsApp Chromium Sandbox + +WhatsApp adapter now launches Chromium in sandboxed mode by default. + +If you must disable Chromium sandboxing in a high-trust/containerized environment: + +```yaml +whatsapp: + no_sandbox: true +``` + ### Gateway API | Method | Description | diff --git a/docs/plans/analysis/2026-02-16-codebase-audit-report.md b/docs/plans/analysis/2026-02-16-codebase-audit-report.md index 8502ad0..f8b9e2d 100644 --- a/docs/plans/analysis/2026-02-16-codebase-audit-report.md +++ b/docs/plans/analysis/2026-02-16-codebase-audit-report.md @@ -13,6 +13,7 @@ Scope: Production-risk-first audit of bugs, code improvements, and feature oppor - ✅ F-010 addressed: `session.compact` audit events now emit actual message counts for `messages_before/messages_after` (tokens remain in token fields). - ✅ F-012 addressed: synthetic repeated-tool nudge no longer emits invalid `tool_result.tool_use_id`; nudge is injected as plain user text guidance. - ✅ F-009 addressed: gateway now enforces per-connection WebSocket ingress rate limits with deterministic throttle errors and close-on-repeated-violation behavior. +- ✅ F-008 addressed: WhatsApp Chromium launch is now sandboxed by default; no-sandbox mode is behind explicit `whatsapp.no_sandbox: true` opt-in. ## Executive Summary diff --git a/docs/plans/state.json b/docs/plans/state.json index f28ffe2..e43a965 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2496,6 +2496,22 @@ "docs/plans/analysis/2026-02-16-codebase-audit-report.md" ], "test_status": "pnpm test:run src/gateway/server.test.ts src/config/schema.test.ts + pnpm typecheck passing" + }, + "audit-followup-whatsapp-sandbox-default": { + "status": "completed", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Hardened WhatsApp adapter Chromium launch defaults: sandbox enabled by default, with explicit opt-in no-sandbox mode via whatsapp.no_sandbox. Added adapter and schema regression tests.", + "files_modified": [ + "src/channels/whatsapp/adapter.ts", + "src/channels/whatsapp/adapter.test.ts", + "src/config/schema.ts", + "src/config/schema.test.ts", + "src/daemon/channels.ts", + "README.md", + "docs/plans/analysis/2026-02-16-codebase-audit-report.md" + ], + "test_status": "pnpm test:run src/channels/whatsapp/adapter.test.ts src/config/schema.test.ts + pnpm typecheck passing" } }, "overall_progress": { diff --git a/src/channels/whatsapp/adapter.test.ts b/src/channels/whatsapp/adapter.test.ts index 72cada4..1c15d47 100644 --- a/src/channels/whatsapp/adapter.test.ts +++ b/src/channels/whatsapp/adapter.test.ts @@ -564,6 +564,37 @@ describe('WhatsAppAdapter', () => { ); }); + it('uses sandboxed Chromium args by default', async () => { + const connectPromise = adapter.connect(); + simulateEvent('ready'); + await connectPromise; + + const { Client } = await import('whatsapp-web.js'); + expect(Client).toHaveBeenCalledWith(expect.objectContaining({ + puppeteer: expect.objectContaining({ + headless: true, + args: [], + }), + })); + }); + + it('allows opting into no-sandbox Chromium args via config', async () => { + const adapterNoSandbox = new WhatsAppAdapter({ + allowNoSandbox: true, + }); + + const connectPromise = adapterNoSandbox.connect(); + simulateEvent('ready'); + await connectPromise; + + const { Client } = await import('whatsapp-web.js'); + expect(Client).toHaveBeenCalledWith(expect.objectContaining({ + puppeteer: expect.objectContaining({ + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }), + })); + }); + it('connect sets error status when initialize() rejects', async () => { mockInitialize.mockRejectedValueOnce(new Error('Browser launch failed')); diff --git a/src/channels/whatsapp/adapter.ts b/src/channels/whatsapp/adapter.ts index 1d70984..2a8a97a 100644 --- a/src/channels/whatsapp/adapter.ts +++ b/src/channels/whatsapp/adapter.ts @@ -32,6 +32,8 @@ export interface WhatsAppAdapterConfig { dataDir?: string; /** Optional pairing manager for DM pairing codes. */ pairingManager?: PairingManager; + /** Allow launching Chromium without sandbox (unsafe; use only in high-trust/containerized setups). */ + allowNoSandbox?: boolean; } /** Minimal shape of a whatsapp-web.js message. */ @@ -88,11 +90,18 @@ export class WhatsAppAdapter implements ChannelAdapter { dataPath: this.config.dataDir, }); + const puppeteerArgs = this.config.allowNoSandbox + ? ['--no-sandbox', '--disable-setuid-sandbox'] + : []; + if (this.config.allowNoSandbox) { + console.warn('WhatsApp adapter: Chromium sandbox disabled via config (unsafe).'); + } + this.client = new Client({ authStrategy, puppeteer: { headless: true, - args: ['--no-sandbox', '--disable-setuid-sandbox'], + args: puppeteerArgs, }, }); diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index 547f408..ec637a8 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -288,6 +288,29 @@ describe('configSchema — matrix', () => { }); }); +describe('configSchema — whatsapp', () => { + const minimalConfig = { + telegram: { bot_token: 'test', allowed_chat_ids: [1] }, + models: { default: { provider: 'anthropic', model: 'claude-3' } }, + }; + + it('defaults whatsapp no_sandbox to false', () => { + const result = configSchema.parse({ + ...minimalConfig, + whatsapp: {}, + }); + expect(result.whatsapp?.no_sandbox).toBe(false); + }); + + it('accepts whatsapp no_sandbox override', () => { + const result = configSchema.parse({ + ...minimalConfig, + whatsapp: { no_sandbox: true }, + }); + expect(result.whatsapp?.no_sandbox).toBe(true); + }); +}); + describe('configSchema — skills watcher', () => { const minimalConfig = { telegram: { bot_token: 'test', allowed_chat_ids: [1] }, diff --git a/src/config/schema.ts b/src/config/schema.ts index 9683ed3..32aea49 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -373,6 +373,7 @@ const whatsappSchema = z.object({ allowed_group_ids: z.array(z.string()).default([]), require_mention: z.boolean().default(true), data_dir: z.string().optional(), + no_sandbox: z.boolean().default(false), }).optional(); const matrixSchema = z.object({ diff --git a/src/daemon/channels.ts b/src/daemon/channels.ts index 7488e8e..46d4e4a 100644 --- a/src/daemon/channels.ts +++ b/src/daemon/channels.ts @@ -66,6 +66,7 @@ export function registerChannels(deps: ChannelsDeps): ChannelsResult { requireMention: config.whatsapp.require_mention, dataDir: config.whatsapp.data_dir, pairingManager, + allowNoSandbox: config.whatsapp.no_sandbox, }); channelRegistry.register(whatsappAdapter); }