fix(whatsapp): sandbox chromium by default
This commit is contained in:
@@ -749,6 +749,17 @@ pairing:
|
|||||||
| `/pair list` | List pending codes and approved senders |
|
| `/pair list` | List pending codes and approved senders |
|
||||||
| `/pair revoke <channel> <id>` | Revoke an approved sender |
|
| `/pair revoke <channel> <id>` | 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
|
### Gateway API
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Description |
|
||||||
|
|||||||
@@ -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-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-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-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
|
## Executive Summary
|
||||||
|
|
||||||
|
|||||||
@@ -2496,6 +2496,22 @@
|
|||||||
"docs/plans/analysis/2026-02-16-codebase-audit-report.md"
|
"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"
|
"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": {
|
"overall_progress": {
|
||||||
|
|||||||
@@ -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 () => {
|
it('connect sets error status when initialize() rejects', async () => {
|
||||||
mockInitialize.mockRejectedValueOnce(new Error('Browser launch failed'));
|
mockInitialize.mockRejectedValueOnce(new Error('Browser launch failed'));
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface WhatsAppAdapterConfig {
|
|||||||
dataDir?: string;
|
dataDir?: string;
|
||||||
/** Optional pairing manager for DM pairing codes. */
|
/** Optional pairing manager for DM pairing codes. */
|
||||||
pairingManager?: PairingManager;
|
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. */
|
/** Minimal shape of a whatsapp-web.js message. */
|
||||||
@@ -88,11 +90,18 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
|||||||
dataPath: this.config.dataDir,
|
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({
|
this.client = new Client({
|
||||||
authStrategy,
|
authStrategy,
|
||||||
puppeteer: {
|
puppeteer: {
|
||||||
headless: true,
|
headless: true,
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
args: puppeteerArgs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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', () => {
|
describe('configSchema — skills watcher', () => {
|
||||||
const minimalConfig = {
|
const minimalConfig = {
|
||||||
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ const whatsappSchema = z.object({
|
|||||||
allowed_group_ids: z.array(z.string()).default([]),
|
allowed_group_ids: z.array(z.string()).default([]),
|
||||||
require_mention: z.boolean().default(true),
|
require_mention: z.boolean().default(true),
|
||||||
data_dir: z.string().optional(),
|
data_dir: z.string().optional(),
|
||||||
|
no_sandbox: z.boolean().default(false),
|
||||||
}).optional();
|
}).optional();
|
||||||
|
|
||||||
const matrixSchema = z.object({
|
const matrixSchema = z.object({
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function registerChannels(deps: ChannelsDeps): ChannelsResult {
|
|||||||
requireMention: config.whatsapp.require_mention,
|
requireMention: config.whatsapp.require_mention,
|
||||||
dataDir: config.whatsapp.data_dir,
|
dataDir: config.whatsapp.data_dir,
|
||||||
pairingManager,
|
pairingManager,
|
||||||
|
allowNoSandbox: config.whatsapp.no_sandbox,
|
||||||
});
|
});
|
||||||
channelRegistry.register(whatsappAdapter);
|
channelRegistry.register(whatsappAdapter);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user