clawdforge/clients/cpp/include/clawdforge/client.hpp
Kayos 1f6606d3b9 clients/cpp: v0.2 multi-turn Session API
- Session move-only RAII; destructor best-effort close
- Client::create_session / list_sessions / get_session
- TurnResult.text() helper
- All post-parse json::get<T>() wrapped via with_protocol_guard (no nlohmann leak)
- tests/test_session.cpp: ~12 tests covering RAII/idempotency/move/list/state/404/protocol-error/regression
- ASan + UBSan clean
- README "Multi-turn / Sessions (v0.2)" section

v0.1 surface unchanged. C++17 preserved. cpp-httplib 0.20.1 preserved.

Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
2026-04-29 07:10:50 -07:00

204 lines
7.9 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 {
class Client;
/// Handle for a multi-turn session on the server (v0.2). Construct via
/// `Client::create_session`. Mirrors `Client`'s move-only RAII shape —
/// destructor best-effort closes the session if it hasn't been closed
/// explicitly, swallowing any exceptions raised on the wire so destruction
/// stays `noexcept`.
///
/// Move-only: each `Session` carries an internal closed-flag, and copying
/// would race the close path. A moved-from `Session` is marked
/// already-closed so the destructor doesn't double-call.
///
/// Not thread-safe. Call `turn()` from one thread at a time per session;
/// different sessions on the same `Client` still share the `Client`'s
/// libcurl handle, so the same per-Client serialization rule applies.
class Session {
public:
~Session();
Session(Session&&) noexcept;
Session& operator=(Session&&) noexcept;
Session(const Session&) = delete;
Session& operator=(const Session&) = delete;
/// Server-assigned session id (PK on the clawdforge ledger).
[[nodiscard]] const std::string& id() const noexcept;
/// Agent name the session was created against (default "claude").
[[nodiscard]] const std::string& agent() const noexcept;
/// Unix timestamp the server recorded at create time.
[[nodiscard]] std::int64_t created_at() const noexcept;
/// `true` after `close()` has run successfully or after move-from.
[[nodiscard]] bool closed() const noexcept;
/// `POST /sessions/{id}/turn`. Returns the structured event batch.
///
/// Throws `Error("session is closed")` if called after `close()` or
/// on a moved-from instance. Other failures follow the SDK's standard
/// exception hierarchy (`AuthError` / `APIError` / `TransportError`
/// / `ProtocolError`).
[[nodiscard]] TurnResult turn(std::string_view prompt,
const TurnOptions& opts = {});
/// `GET /sessions/{id}`. Live state fetch.
[[nodiscard]] SessionState state();
/// `DELETE /sessions/{id}`. Idempotent — a second call short-circuits
/// without an HTTP round-trip via the internal closed flag. The server's
/// close endpoint is itself idempotent, but the local short-circuit
/// saves the request in the common destructor / explicit-close pair
/// pattern.
void close();
private:
friend class Client;
Session(Client* client, std::string id, std::string agent,
std::int64_t created_at);
struct Impl;
std::unique_ptr<Impl> impl_; ///< PImpl: keeps libcurl out of the header.
};
/// 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);
// -- v0.2: multi-turn session API --------------------------------------
/// `POST /sessions`. Returns a move-only `Session` handle whose
/// destructor best-effort closes the session on the server.
[[nodiscard]] Session create_session(const CreateSessionOptions& opts = {});
/// `GET /sessions`. Returns every session visible to the calling token
/// (per-app isolation enforced server-side). Includes closed-but-not-
/// hard-deleted sessions by default.
[[nodiscard]] std::vector<SessionState> list_sessions();
/// `GET /sessions/{id}`. Throws `APIError` with `status_code() == 404`
/// for unknown ids OR for ids that exist under a different token
/// (no-existence-leak design).
[[nodiscard]] SessionState get_session(std::string_view id);
// -- internal helpers used by Session ----------------------------------
//
// Public on the type (the surface lives in <clawdforge/client.hpp>) but
// not part of the documented user API — see `Session::turn` / `close`
// / `state` for the user-facing shape. Direct calls bypass the local
// closed-flag short-circuit and the move-from guard.
/// `POST /sessions/{id}/turn`. Used by `Session::turn`.
[[nodiscard]] TurnResult session_turn_internal(std::string_view id,
std::string_view prompt,
const TurnOptions& opts);
/// `DELETE /sessions/{id}`. Used by `Session::close` and the destructor.
void session_close_internal(std::string_view id);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace clawdforge