# clawdforge — C SDK A small synchronous C client for the [clawdforge](../../README.md) HTTP service. clawdforge wraps `claude -p` subprocess calls behind a bearer-token-gated REST API; this SDK is what you reach for from C, or what you FFI into from any language with a C ABI. - **C99 minimum, C11 preferred.** No GNU extensions in the public surface. - **Dependencies:** [libcurl](https://curl.se/libcurl/) (system) + [cJSON](https://github.com/DaveGamble/cJSON) (vendored under `src/cjson/`). - **Errors are out-params**, not globals. Library functions never write to `stderr`. - **All allocations are caller-aware** — every output struct ships with a `_free()`. - **Thread-safety:** one `cf_client_t` per thread. They are not internally locked. - License: MIT (see `CMakeLists.txt`). ## Install Build deps on Debian/Ubuntu: ``` sudo apt install build-essential cmake libcurl4-openssl-dev pkg-config ``` Then: ``` git clone /Sulkta-Coop/clawdforge.git cd clawdforge/clients/c cmake -S . -B build cmake --build build -j sudo cmake --install build --prefix /usr/local ``` This installs: - `libclawdforge.a` and `libclawdforge.so.0.1.0` into `${prefix}/lib` - `clawdforge.h` into `${prefix}/include` - `clawdforge.pc` into `${prefix}/lib/pkgconfig` ## Use it from your build ### pkg-config ``` gcc app.c $(pkg-config --cflags --libs clawdforge) -o app ``` ### CMake If you installed to a custom prefix, point `CMAKE_PREFIX_PATH` at it. Then: ```cmake find_package(PkgConfig REQUIRED) pkg_check_modules(CLAWDFORGE REQUIRED IMPORTED_TARGET clawdforge) target_link_libraries(my_app PRIVATE PkgConfig::CLAWDFORGE) ``` ## Quickstart ```c #include #include int main(void) { cf_client_t *c = cf_client_new("http://192.168.0.5:8800", "cf_xxx"); if (!c) return 1; cf_error_t err = {0}; /* Health check */ cf_healthz_t h = {0}; if (cf_healthz(c, &h, &err) != CF_OK) { fprintf(stderr, "%s\n", cf_error_message(&err)); cf_error_free(&err); cf_client_free(c); return 1; } printf("claude: %s\n", h.claude_version); cf_healthz_free(&h); /* Run a prompt */ cf_run_request_t req = { .prompt = "Reply with JSON: {\"hello\":\"world\"}", .model = "sonnet", .timeout_secs = 60, }; cf_run_result_t res = {0}; if (cf_run(c, &req, &res, &err) != CF_OK) { fprintf(stderr, "%s\n", cf_error_message(&err)); cf_error_free(&err); cf_client_free(c); return 1; } printf("%s\n", res.result_json); /* {"hello":"world"} */ printf("%ld ms\n", res.duration_ms); cf_run_result_free(&res); cf_client_free(c); return 0; } ``` ## Memory rules The library never holds onto pointers past the call boundary. The pattern is: 1. **You** pass `const char *` strings in. The library copies what it needs. 2. **The library** populates output structs. Their string members are heap allocated and owned by the caller. 3. **You** call the matching `*_free()` to release them. ```c cf_run_result_t res = {0}; /* zero-initialise */ cf_run(c, &req, &res, &err); /* fills it */ /* ... use res.result_json ... */ cf_run_result_free(&res); /* releases it */ ``` The `cf_error_t` follows the same pattern: zero-initialise on the stack, pass by address, free on failure. Do not call `cf_error_free()` after a successful call (the struct was not touched). ## Status codes ```c typedef enum cf_status { CF_OK = 0, CF_ERR_AUTH = 1, /* 401 / 403 */ CF_ERR_API = 2, /* server returned a structured error envelope */ CF_ERR_TRANSPORT = 3, /* network / TLS / connection */ CF_ERR_USAGE = 4, /* caller passed a bad argument */ CF_ERR_OOM = 5, /* out of memory */ CF_ERR_PARSE = 6 /* server response was not the expected JSON shape */ } cf_status_t; ``` Use `cf_status_str(code)` for a human label. ## API reference ### Lifecycle | Function | Notes | | --- | --- | | `cf_client_t *cf_client_new(const char *base_url, const char *token)` | Allocates a client. `token` may be `NULL` if you only intend to use admin endpoints. Trailing slashes on `base_url` are stripped. | | `cf_status_t cf_client_set_admin_token(cf_client_t *c, const char *admin_token)` | Required before any `cf_admin_*` call. | | `void cf_client_set_timeout_secs(cf_client_t *c, long timeout_secs)` | Default 600 seconds. | | `void cf_client_free(cf_client_t *c)` | Releases. | ### `/healthz` ```c cf_status_t cf_healthz(cf_client_t *c, cf_healthz_t *out, cf_error_t *err); void cf_healthz_free(cf_healthz_t *h); ``` `cf_healthz_t` carries `ok`, `claude_present`, and `claude_version` (heap str). ### `/run` ```c cf_status_t cf_run(cf_client_t *c, const cf_run_request_t *req, cf_run_result_t *out, cf_error_t *err); void cf_run_result_free(cf_run_result_t *r); void *cf_run_result_as_cjson(const cf_run_result_t *r); ``` `cf_run_result_t::result_json` is the **inner** `result` field re-serialised back to a JSON string. If the server's `result` was a string, this is a quoted JSON string (e.g. `"\"hello\""`). If the server returned an object, this is the object's JSON. `result_is_string` distinguishes the two. `cf_run_result_as_cjson()` returns a freshly-parsed `cJSON *` (cast it). The caller must `cJSON_Delete()` the returned node. The header returns `void *` so consumers don't need cJSON visible. ### `/files` ```c cf_status_t cf_upload_file(cf_client_t *c, const char *path, long ttl_secs, /* 0 or 60..86400 */ cf_file_token_t *out, cf_error_t *err); void cf_file_token_free(cf_file_token_t *ft); ``` Uploads stream from disk via `curl_mime_filedata` — files are NOT loaded into memory. ### `/admin/tokens` ```c cf_status_t cf_admin_create_token(cf_client_t *c, const char *name, const char *const *ip_cidrs, size_t ip_cidrs_count, cf_admin_token_t *out, cf_error_t *err); cf_status_t cf_admin_list_tokens(cf_client_t *c, cf_admin_token_list_t *out, cf_error_t *err); cf_status_t cf_admin_revoke_token(cf_client_t *c, const char *name, cf_error_t *err); ``` All require an admin token via `cf_client_set_admin_token` first. ## Build options | Option | Default | What it does | | --- | --- | --- | | `-DCLAWDFORGE_BUILD_TESTS=ON/OFF` | `ON` | Build `clawdforge_tests`. | | `-DCLAWDFORGE_BUILD_EXAMPLES=ON/OFF` | `ON` | Build `clawdforge_example_basic`. | | `-DCLAWDFORGE_ASAN=ON/OFF` | `OFF` | Build the lib + tests with `-fsanitize=address`. | ## Tests ``` cmake -S . -B build cmake --build build -j cd build && ctest --output-on-failure ``` The test suite spins up an in-process HTTP server on a loopback random port and exercises the wire surface: URL construction, auth header injection, request/response JSON shapes, error envelopes, and multipart upload. To run under AddressSanitizer: ``` cmake -S . -B build-asan -DCLAWDFORGE_ASAN=ON cmake --build build-asan -j cd build-asan && ctest --output-on-failure ``` To run under valgrind: ``` valgrind --leak-check=full --error-exitcode=1 ./build/clawdforge_tests ``` ## Threading `cf_client_t` is **not** safe to share across threads. Each thread should own its own client (or guard one with your own mutex). Internally, every call constructs and tears down its own libcurl easy handle, so adding a shared connection pool is a future enhancement that won't change the API. ## Vendored cJSON `src/cjson/cJSON.{c,h}` are vendored from cJSON v1.7.15 (MIT). The license file lives next to them.