Response to incident WA-2026-10-27 (the std::vector max_size() crash). Deep research archived under research/safe-cpp-practices (8 Tier 1, 37 Tier 2, 2 conclusions) on production-safe C++ that keeps development speed: OpenSSF compiler hardening, the C++ Core Guidelines bounds profile, integer-overflow practice, libFuzzer/sanitizer CI, the sanitizer overhead ladder, clang-tidy ratchets. The research prescribed an ASan+UBSan+_GLIBCXX_ASSERTIONS build of the parsers. Standing it up immediately caught a second, independent undefined-behavior bug that two manual reviews had missed: a protobuf varint loop that shifted by up to 70 bits (shift >= 64 is UB) on malformed input. Fixed and guarded by a deterministic test and the sanitizer net. - src/whatsapp/wa_proto.cpp: guard the wire-0 varint shift (the UB). - tests/whatsapp_test.cpp: add an overlong-varint hostile case. - tools/san-whatsapp.sh: the standing sanitizer net for detz::wa parsers. - WaClient frame capture: a frame that fails to decode/dispatch has its exact bytes recorded to <home>/whatsapp/last-error.log (bounded), so the still unreproduced live <iq> failure becomes a one-paste deterministic fix. Wired from the wizard and the whatsapp_send tool. - docs/cpp-safety.md: the project-wide playbook and design decisions (four layers: overflow-safe checked-reader parsing, cheap release hardening, the sanitizer test net, capture). docs/whatsapp.md section 13.6 records the follow-up and the honest open item. AGENTS.md makes it a standing rule; cap.md and mcp.md inherit it for their wire parsers. Verified: build, whatsapp_test (incl. hostile+fuzz), make lint, and tools/san-whatsapp.sh all green; research audit PASS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
3.4 KiB
Markdown
67 lines
3.4 KiB
Markdown
# mcp (taicpp)
|
|
|
|
Port of the MCP section of `../tai.py`, normative text in `../mcp.md`.
|
|
Namespace `detz::mcp`, sources in `src/mcp/`. Reuses the `detz::wire` layer
|
|
from `src/cap/` (see `docs/cap.md`).
|
|
|
|
**Wire parsing is untrusted input**: every byte here comes off a socket, so the parsers follow [cpp-safety.md](cpp-safety.md) (overflow-safe size checks, unsigned range-checked lengths, guarded shifts, no exception escaping the read loop, hostile-input tests under sanitizers). We do not accept regressions in that.
|
|
|
|
## Spec delta (this milestone)
|
|
|
|
The placeholder `serve_list`/`serve_call` is replaced by the real client,
|
|
OAuth, bridge, vault tables, tool backends and server.
|
|
|
|
## Parity table
|
|
|
|
| Python | C++ |
|
|
|---|---|
|
|
| `MCP_PROTOCOL_VERSION` `MCP_CLIENT_NAME` `MCP_TOOL_PREFIX` `MCP_RESOLVER_TIMEOUT` `MCP_RESOLVER_STEPS` `MCP_OAUTH_TIMEOUT` `MCP_RESOLVER_BRIEF` `MCP_CONNECT_TIMEOUT` `MCP_CALL_TIMEOUT` `MCP_RESULT_MAX` `MCP_HTTP_MAX_BODY` `MCP_MAX_SERVERS` `MCP_MAX_TOOLS` `MCP_SESSION_IDLE` `MCP_CHAT_TIMEOUT` `MCP_DEFAULT_PORT` `MCP_SERVER_RE` `MCP_VAR_RE` `MCP_GITHUB_RE` | `mcp_constants.h` |
|
|
| `McpError` `McpJsonRpcError` `McpHttpError` | `mcp_error.h` |
|
|
| `mcp_sanitize_name` `mcp_github_target` `mcp_expand_vars` `mcp_rewrite_placeholders` `mcp_entry_from_mapping` `mcp_normalize` `mcp_tool_hash` `mcp_bridge_name` | `McpNormalizer` |
|
|
| `McpStdioTransport` `McpHttpTransport` `McpClient` | same names, `McpTransport` interface |
|
|
| `McpOAuthCallback` `McpOAuth` | same names |
|
|
| `McpBridge` | `McpBridge` over `McpHost` |
|
|
| `Store.mcp_*` | `McpStore` interface, `McpSqliteStore` (four tables) |
|
|
| `Tools.run_mcp_*`, `mcp_resolve_github`, `mcp_resolve_agent`, `mcp_schemas` | `McpTools` |
|
|
| `McpServeSession` `McpServeContext` `mcp_serve_tools` `mcp_serve_handler` `mcp_serve_main` | `McpServeSession` `McpServeContext` `McpServeHandler` `McpServeMain` |
|
|
|
|
## Wire subset
|
|
|
|
Methods: `initialize`, `notifications/initialized`, `ping`, `tools/list`,
|
|
`tools/call`, `notifications/cancelled`. Errors `-32601`, `-32602`,
|
|
`-32603`. Protocol `2025-11-25`.
|
|
|
|
## Caps
|
|
|
|
`MCP_CONNECT_TIMEOUT` 20, `MCP_CALL_TIMEOUT` 60, `MCP_RESULT_MAX` 65536,
|
|
`MCP_HTTP_MAX_BODY` 10485760, `MCP_MAX_SERVERS` 32, `MCP_MAX_TOOLS` 100,
|
|
`MCP_SESSION_IDLE` 3600, `MCP_CHAT_TIMEOUT` 600, `MCP_RESOLVER_TIMEOUT` 300,
|
|
`MCP_RESOLVER_STEPS` 12, `MCP_OAUTH_TIMEOUT` 300.
|
|
|
|
## Vault tables
|
|
|
|
`mcp_servers`, `mcp_server_profiles`, `mcp_tool_pins`, `mcp_server_tools`
|
|
(`McpSqliteStore::ensureSchema`).
|
|
|
|
## Agent tools
|
|
|
|
`mcp_add`, `mcp_list`, `mcp_enable`, `mcp_disable`, `mcp_remove`,
|
|
`mcp_tools`, `mcp_call`, `mcp_login` (`McpTools::dispatch`). Served extras:
|
|
`chat`, `completions`.
|
|
|
|
## Differences from Python (declared)
|
|
|
|
- `mcp_login` browser: opens `xdg-open` only when `DISPLAY`/`WAYLAND_DISPLAY`
|
|
is set; Python's `webbrowser` has a longer fallback list.
|
|
- Transport error texts come from libcurl mapped to urllib wording
|
|
(`[Errno 111] Connection refused`, `timed out`); exotic failures differ.
|
|
- HTTP `Server:` header omits the Python version suffix.
|
|
- `mcp_store_tools` runs in one transaction (Python leaves partial writes
|
|
pending on a duplicate tool name).
|
|
- stdio stderr is a pipe nobody drains, exactly as in tai.py (the doc says
|
|
drained, the code does not).
|
|
|
|
## Verification
|
|
|
|
`tests/capmcp_unit.cpp` (vault tables, normalizer, served endpoint in process). No differential run against tai.py yet for the client, OAuth, bridge and server (see docs/wiring/capmcp.md, Known gaps).
|