Files
codebase-memory-mcp/src/cli/client_adapter.h
T
Martin Vogel a6f132bc85 harden: ship integration templates as a verified JSON asset, not embedded bytes
The product binary embedded nine complete shebang'd shell scripts, their
PowerShell/.cmd twins, and two node:child_process client-adapter modules as C
string literals, and wrote them into agent config dirs at 0755 on install. A
block of script-shaped bytes inside a large unsigned executable is exactly the
surface Microsoft Defender's ML scored Trojan:Script/Wacatac.B!ml — the
detection that has recurred across the last several release attempts, on the
Linux ELF and darwin-arm64 standard binaries while every UI and Windows variant
stayed clean.

This moves every integration template out of the binary into a single shipped
data file, assets/cbm-integrations.json (compact: one asset, because each
shipped file is itself scanned). The binary now carries only the templates'
identity — one embedded SHA-256, generated at build time from the JSON so it
can never drift — plus the code to load, verify, and render them.

Why a hash and not just a loose file: content compiled into the binary was
tamper-evident with the binary. Moving it to disk must not turn it into
unauthenticated code the installer blindly executes. install verifies the asset
against the embedded SHA-256 and fails closed on mismatch ("integration assets
missing or modified - reinstall from the release archive") before writing
anything. Verified end to end: a one-byte edit to the asset makes install
refuse.

Ownership, for safe upgrade/removal: the verified copy is published to
~/.cbm/assets/<version>/ — a sibling of the DB cache, never inside it, so
clearing the cache cannot strip a user's hooks. A deployed integration file is
ours to remove only if it matches a template rendered from that stored copy;
foreign files and user-edited files at the reserved paths are preserved. The
historical template bodies travel in the JSON's released[] arrays so files
written by older versions are still recognised and cleaned up on uninstall.

Removed from src/: cmm_gate/session/subagent script-prefix and suffix
constants, cbm_build_released_gate_script, the shell/PowerShell/.cmd bodies in
cli.c, and the JS/TS generators in client_adapter.c. Confirmed on the built
standard binary: 0 occurrences of '#!/usr/bin/env bash', '#!/bin/sh',
'node:child_process', and 'ExecutionPolicy' (was non-zero for each).

test_no_embedded_scripts_contract.sh makes the removal a build property, not a
reachability assumption: no production C source may contain a shebang or
node:child_process literal, and the built binary is scanned directly. It strips
comments first (a contract a comment can satisfy is a false guard), and is
revert-checked — reintroducing one shebang literal makes it fail naming the
file:line. check-binary-composition.sh (A6) enforces the same needles on every
release artifact post-strip.

This does not, by itself, prove the detection is cured: the clean UI binary is a
strict superset of the flagged standard one, so no single passage is a
deterministic trigger. It removes the most coherent malware-shaped composite
from every shipped binary, aligns with how gh/starship/zoxide externalise
integration material, and makes the templates maintainable as data rather than
escaped C literals. Causal confirmation still needs a VirusTotal ablation.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-08 02:58:20 +02:00

77 lines
3.9 KiB
C

/*
* client_adapter.h — Generate client extension modules from the MCP tool registry.
*
* Some clients have no MCP support (pi), and some have no declarative hook
* configuration (OpenCode); their only extension point is a JavaScript/TypeScript
* module. Generation splits along what each source is authoritative for:
*
* - The module SCAFFOLDING (the node:child_process spawn bridge, header note,
* plugin surface) comes from the hash-verified cbm-integrations.json via
* integration_assets.h. It used to be compiled in as C string literals, but
* child-process-spawning script text inside an unsigned binary is exactly
* the surface AV classifiers score, so it ships as data and is verified
* against the binary's embedded SHA-256 before use.
*
* - The TOOL LIST is still generated at install time from the live registry
* (cbm_mcp_tool_count / cbm_mcp_tool_name), never hand-maintained in the
* asset file: hand-maintained copies of the tool surface have already
* produced defects here. Adding a tool to TOOLS[] adds it to every
* generated adapter with no second edit — that property is preserved.
*
* Generated text is returned heap-allocated; the caller frees it. Nothing here
* touches the filesystem — writing is the caller's job, so dry-run and
* owned-block semantics stay in one place.
*/
#ifndef CBM_CLI_CLIENT_ADAPTER_H
#define CBM_CLI_CLIENT_ADAPTER_H
#include <stdbool.h>
#include <stddef.h>
/* Marker lines delimiting the generated region inside a client's extension
* module. A file carrying these is ours to rewrite; one without them is the
* user's and is left alone. Kept in the comment syntax shared by JS and TS. */
#define CBM_ADAPTER_MARKER_START "// codebase-memory-mcp:start"
#define CBM_ADAPTER_MARKER_END "// codebase-memory-mcp:end"
/* Emit a pi extension registering every tool in the MCP registry.
*
* pi has no MCP client, so this bridge is the only route to the graph for it.
* Each registered tool shells out to `<binary> cli <tool> '<json-args>'`, which
* is an existing public entry point rather than a private one.
*
* `binary_path` is embedded as a JS string literal and is escaped; a Windows
* path such as C:\Users\x\bin\cbm.exe must not corrupt the module or, worse,
* produce a file that fails to parse inside an auto-loaded extension directory.
*
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
char *cbm_client_adapter_pi(const char *binary_path);
/* Emit an OpenCode plugin that augments grep/glob results from the graph.
*
* OpenCode HAS MCP (see cbm_upsert_opencode_mcp), so this adds no tools — it
* only supplies the automatic "consult the graph before grepping" reflex that
* other clients get through their own native hook configuration. OpenCode has
* no such configuration; a plugin module is its only extension point.
*
* NOTE for maintainers: this hooks `tool.execute.after`, whose ability to
* modify a tool's output is NOT part of OpenCode's documented plugin contract
* (only `tool.execute.before`'s argument mutation is). If OpenCode changes it,
* augmentation stops silently — no error, no signal. That risk is accepted
* deliberately and recorded here so it is not rediscovered as a mystery.
*
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
char *cbm_client_adapter_opencode(const char *binary_path);
/* Escape a path for embedding in a single-quoted JS string literal: backslash,
* single quote, and the newline/CR that would terminate the literal outright.
* Writes at most out_sz-1 characters plus NUL. Returns false when the escaped
* form would not fit, so a caller never emits a truncated literal.
*
* Exposed for tests: the escaping is the part most likely to break a user's
* install on Windows, and it deserves direct coverage rather than only being
* reachable through a full generate call. */
bool cbm_client_adapter_escape_js(const char *in, char *out, size_t out_sz);
#endif /* CBM_CLI_CLIENT_ADAPTER_H */