Synchronous client over libcurl + vendored cJSON. Single public
header (include/clawdforge.h) with an opaque cf_client_t and the
full surface: /healthz, /run, /files, /admin/tokens.
- C11, no GNU extensions; -Wall -Wextra -Wpedantic clean
- Hidden visibility on the shared lib + CF_API export attribute
- Static + shared lib via CMake; relocatable pkg-config (${pcfiledir})
- Errors via out-param cf_error_t; every output struct has a _free()
- Multipart upload streams from disk via curl_mime_filedata
- 15 in-process socket-loop tests; valgrind + ASan clean
7.9 KiB
clawdforge — C SDK
A small synchronous C client for the clawdforge 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 (system) + 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_tper 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 <gitea-url>/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.aandlibclawdforge.so.0.1.0into${prefix}/libclawdforge.hinto${prefix}/includeclawdforge.pcinto${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:
find_package(PkgConfig REQUIRED)
pkg_check_modules(CLAWDFORGE REQUIRED IMPORTED_TARGET clawdforge)
target_link_libraries(my_app PRIVATE PkgConfig::CLAWDFORGE)
Quickstart
#include <stdio.h>
#include <clawdforge.h>
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:
- You pass
const char *strings in. The library copies what it needs. - The library populates output structs. Their string members are heap allocated and owned by the caller.
- You call the matching
*_free()to release them.
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
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
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
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
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
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.