- 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
204 lines
7.9 KiB
C++
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
|