clawdforge/clients/cpp/CMakeLists.txt
Kayos 19fe299b3d clients/cpp: apply audit findings — protocol-error guard + libcurl redirect clamp (bae34a7 → next)
HIGH:
- H1: nlohmann::json::exception wrapped as ProtocolError at 5 sites in
  client.cpp via with_protocol_guard helper. Preserves the documented
  clawdforge::Error catch-all base contract; nlohmann types never leak
  into the message (e.what() only).
- H2: libcurl MAXREDIRS=5, REDIR_PROTOCOLS_STR="http,https"
  (CURLOPT_REDIR_PROTOCOLS bitmask fallback for libcurl < 7.85.0),
  UNRESTRICTED_AUTH=0L. Defense-in-depth on top of libcurl's automatic
  bearer strip on cross-host redirects (>=7.64.0).

MEDIUM:
- M1: upload_file resolves the path via std::filesystem::canonical up
  front. Closes broken-symlink, symlink-loop, and TOCTOU-on-target
  classes without a doc burden on callers.
- M2: README "Linking" section documents the public-ABI nlohmann_json
  implication. v0.2 wrapper deferred.
- M3: README "Threat model" section documents the parse-depth concern
  on the result field of /run replies. Runtime guard skipped for v0.1
  per audit recommendation (low yield, complexity).

LOW:
- L1: cxx_std_20 → cxx_std_17 in CMakeLists.txt (no C++20-only
  features in the library source; broader downstream reach). Examples
  and tests still build via designated initializers (g++ accepts these
  in C++17 mode).
- L2: RunResult struct doc clarifies that missing ok/duration_ms
  decode to defaults — opt-out forward-compat.
- L3: Client class doc clarifies that moved-from instances must not
  have any non-special-member methods invoked (UB), with explicit
  callout on base_url() returning an internal reference.

Test-only:
- cpp-httplib 0.15.3 → 0.20.1. Optional backends (OpenSSL / zlib /
  brotli / zstd) forced off to keep the dep graph minimal. Test-only,
  never on the consumer wire path. README "Test deps" section added
  for transparency.

Tests added (12 → 23 cases, 70 → 106 assertions):
- protocol_error on malformed response for healthz, run, upload_file,
  create_token, list_tokens (H1 regression)
- redirect_clamp_test (H2 regression — TransportError after 5+ hops)
- redirect_protocol_clamp (H2 regression — ftp:// Location rejected)
- upload_file_canonicalize: symlink→file works, broken symlink
  rejected, symlink loop rejected, directory rejected (M1 regression)

Verified:
- cmake --build build clean (-Wall -Wextra -Wpedantic -Wshadow
  -Wconversion -Wsign-conversion -Wold-style-cast -Werror)
- ctest --output-on-failure all green (Release)
- ASan + UBSan: 23/23 cases, 106/106 assertions, zero diagnostics

Audit: memory/clawdforge-audits/cpp-bae34a7.md
2026-04-28 23:41:41 -07:00

214 lines
7 KiB
CMake

# SPDX-License-Identifier: MIT
cmake_minimum_required(VERSION 3.20)
project(clawdforge
VERSION 0.1.0
DESCRIPTION "C++ SDK for the clawdforge HTTP API"
LANGUAGES CXX
)
# ---- options ----------------------------------------------------------------
option(CLAWDFORGE_BUILD_TESTS "Build unit tests" ON)
option(CLAWDFORGE_BUILD_EXAMPLES "Build examples" ON)
option(CLAWDFORGE_WARNINGS_AS_ERRORS "Treat warnings as errors" ON)
# When consumed via add_subdirectory, default the extras off unless the user
# explicitly opts in. Top-level builds (the SDK's own CI / dev loop) keep them
# on so we exercise them.
if(NOT CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
set(CLAWDFORGE_BUILD_TESTS OFF)
set(CLAWDFORGE_BUILD_EXAMPLES OFF)
endif()
# ---- standard / flags -------------------------------------------------------
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
include(FetchContent)
# ---- dependencies -----------------------------------------------------------
# nlohmann/json — try find_package first, fall back to FetchContent.
find_package(nlohmann_json 3.10 QUIET)
if(NOT nlohmann_json_FOUND)
message(STATUS "clawdforge: nlohmann_json not installed, fetching v3.11.3")
FetchContent_Declare(nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
GIT_SHALLOW TRUE
)
set(JSON_BuildTests OFF CACHE INTERNAL "")
set(JSON_Install ON CACHE INTERNAL "")
FetchContent_MakeAvailable(nlohmann_json)
endif()
# libcurl — system-installed.
find_package(CURL REQUIRED)
# ---- library target ---------------------------------------------------------
add_library(clawdforge)
add_library(clawdforge::clawdforge ALIAS clawdforge)
target_sources(clawdforge
PRIVATE
src/client.cpp
src/http.cpp
)
target_include_directories(clawdforge
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_link_libraries(clawdforge
PUBLIC
nlohmann_json::nlohmann_json
PRIVATE
CURL::libcurl
)
target_compile_features(clawdforge PUBLIC cxx_std_17)
set_target_properties(clawdforge PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
POSITION_INDEPENDENT_CODE ON
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
EXPORT_NAME clawdforge
)
# Warnings -- private so consumers don't inherit our flags.
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(clawdforge PRIVATE
-Wall -Wextra -Wpedantic
-Wshadow -Wconversion -Wsign-conversion
-Wnon-virtual-dtor -Wold-style-cast
)
if(CLAWDFORGE_WARNINGS_AS_ERRORS)
target_compile_options(clawdforge PRIVATE -Werror)
endif()
elseif(MSVC)
target_compile_options(clawdforge PRIVATE /W4 /permissive-)
if(CLAWDFORGE_WARNINGS_AS_ERRORS)
target_compile_options(clawdforge PRIVATE /WX)
endif()
endif()
# ---- install ----------------------------------------------------------------
install(DIRECTORY include/clawdforge
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.hpp"
)
install(TARGETS clawdforge
EXPORT clawdforge-targets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(EXPORT clawdforge-targets
FILE clawdforge-targets.cmake
NAMESPACE clawdforge::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/clawdforge
)
configure_package_config_file(
cmake/clawdforge-config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/clawdforge-config.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/clawdforge
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/clawdforge-config-version.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/clawdforge-config.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/clawdforge-config-version.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/clawdforge
)
# ---- examples ---------------------------------------------------------------
if(CLAWDFORGE_BUILD_EXAMPLES)
add_executable(clawdforge_basic_example examples/basic.cpp)
target_link_libraries(clawdforge_basic_example PRIVATE clawdforge::clawdforge)
endif()
# ---- tests ------------------------------------------------------------------
if(CLAWDFORGE_BUILD_TESTS)
enable_testing()
# cpp-httplib for an in-process mock server. Prefer system / installed copy.
# Test-only — never linked into the shipped library. Bumped from 0.15.3 to
# 0.20.1 for clean dep graphs (0.15.x has several published advisories;
# the SDK never exposes the mock to a network).
find_package(httplib QUIET CONFIG)
if(NOT httplib_FOUND)
message(STATUS "clawdforge: cpp-httplib not installed, fetching v0.20.1")
# The mock server doesn't need TLS / compression — disable optional
# backends so we don't drag in zstd / brotli / openssl find_package
# requirements for a test-only dep.
set(HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF CACHE INTERNAL "")
set(HTTPLIB_USE_ZLIB_IF_AVAILABLE OFF CACHE INTERNAL "")
set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF CACHE INTERNAL "")
set(HTTPLIB_USE_ZSTD_IF_AVAILABLE OFF CACHE INTERNAL "")
FetchContent_Declare(cpp_httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.20.1
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(cpp_httplib)
endif()
# doctest — single-header, fastest to compile of the popular options.
find_package(doctest QUIET CONFIG)
if(NOT doctest_FOUND)
message(STATUS "clawdforge: doctest not installed, fetching v2.4.11")
FetchContent_Declare(doctest
GIT_REPOSITORY https://github.com/doctest/doctest.git
GIT_TAG v2.4.11
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(doctest)
endif()
add_executable(clawdforge_tests tests/test_client.cpp)
target_link_libraries(clawdforge_tests
PRIVATE
clawdforge::clawdforge
httplib::httplib
doctest::doctest
)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(clawdforge_tests PRIVATE
-Wall -Wextra
# Designated initializers in C++20 don't need to set every member.
-Wno-missing-field-initializers
# CHECK_THROWS_AS expands to a discarded-value expression; the
# `[[nodiscard]]` on Client methods makes that legitimately noisy.
-Wno-unused-result
)
endif()
add_test(NAME clawdforge_tests COMMAND clawdforge_tests)
endif()