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
112 lines
3.8 KiB
C++
112 lines
3.8 KiB
C++
// SPDX-License-Identifier: MIT
|
|
//
|
|
// Public client surface for the clawdforge HTTP API.
|
|
//
|
|
// Construct a `Client` once, hold onto it, call methods. Throws on failure —
|
|
// see `error.hpp` for the exception hierarchy.
|
|
|
|
#pragma once
|
|
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include <clawdforge/error.hpp>
|
|
#include <clawdforge/types.hpp>
|
|
|
|
namespace clawdforge {
|
|
|
|
/// Knobs for `Client` construction. Use designated initializers to set only
|
|
/// what you care about.
|
|
struct ClientOptions {
|
|
/// Required. e.g. `"http://localhost:8800"` or `"http://192.168.0.5:8800"`.
|
|
/// A trailing slash is fine — it gets normalized away.
|
|
std::string base_url;
|
|
|
|
/// Bearer token used for `/run`, `/files`, `/healthz`. Optional — leave
|
|
/// empty if the client is admin-only.
|
|
std::string token;
|
|
|
|
/// Bearer token for `/admin/*` endpoints. Optional — leave empty when not
|
|
/// doing token CRUD.
|
|
std::string admin_token;
|
|
|
|
/// Per-request timeout. Default 120 s leaves headroom over the server's
|
|
/// default 60 s `claude` subprocess budget.
|
|
std::chrono::seconds timeout{120};
|
|
|
|
/// Connection establishment timeout. Default 10 s.
|
|
std::chrono::seconds connect_timeout{10};
|
|
|
|
/// Override the User-Agent header.
|
|
std::string user_agent;
|
|
|
|
/// Skip TLS verification. Off by default; only useful for self-signed
|
|
/// LAN-internal certs.
|
|
bool insecure_tls{false};
|
|
};
|
|
|
|
/// HTTP client for clawdforge.
|
|
///
|
|
/// Move-only: each `Client` owns a libcurl easy handle, which doesn't share
|
|
/// well between threads. Construct one per worker thread, or guard external
|
|
/// access with a mutex.
|
|
///
|
|
/// Moved-from state: a `Client` that has been moved from holds no resources
|
|
/// and **must not** have any of its non-special-member methods invoked. Doing
|
|
/// so (including `base_url()`) is undefined behaviour. Re-assign or destroy
|
|
/// the moved-from object before further use.
|
|
class Client {
|
|
public:
|
|
explicit Client(ClientOptions opts);
|
|
~Client();
|
|
|
|
// Move-only.
|
|
Client(const Client&) = delete;
|
|
Client& operator=(const Client&) = delete;
|
|
Client(Client&&) noexcept;
|
|
Client& operator=(Client&&) noexcept;
|
|
|
|
/// Base URL the client was configured with (trailing slash trimmed).
|
|
///
|
|
/// Returns a reference into the `Client`'s internal state; the reference
|
|
/// is valid until the `Client` is destroyed or moved from. Do not call
|
|
/// on a moved-from `Client` (UB — see class doc).
|
|
[[nodiscard]] const std::string& base_url() const noexcept;
|
|
|
|
// -- public API --------------------------------------------------------
|
|
|
|
/// `GET /healthz`. Server still enforces the global IP allowlist.
|
|
[[nodiscard]] HealthzResponse healthz();
|
|
|
|
/// `POST /run`. Throws `APIError` on 502 — inspect `body()` for the
|
|
/// `RunFailure` JSON.
|
|
[[nodiscard]] RunResult run(const RunRequest& req);
|
|
|
|
/// `POST /files`. Streams the file via `curl_mime_filedata` — the SDK
|
|
/// does not slurp it into memory.
|
|
///
|
|
/// `ttl_secs == 0` lets the server pick its default (3600). Otherwise
|
|
/// must be in 60..86400.
|
|
[[nodiscard]] FileToken upload_file(std::string_view path,
|
|
std::int32_t ttl_secs = 0);
|
|
|
|
/// `POST /admin/tokens`. Requires `admin_token` on the client.
|
|
[[nodiscard]] AppToken create_token(const TokenCreateRequest& req);
|
|
|
|
/// `GET /admin/tokens`. Requires `admin_token` on the client.
|
|
[[nodiscard]] std::vector<AppTokenInfo> list_tokens();
|
|
|
|
/// `DELETE /admin/tokens/{name}`. Requires `admin_token` on the client.
|
|
/// Throws `APIError` with `status_code() == 404` if the token is unknown.
|
|
void revoke_token(std::string_view name);
|
|
|
|
private:
|
|
struct Impl;
|
|
std::unique_ptr<Impl> impl_;
|
|
};
|
|
|
|
} // namespace clawdforge
|