clawdforge/clients/c/README.md
Kayos a69e924592 clients/c: initial C SDK for clawdforge
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
2026-04-28 23:01:52 -07:00

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_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 <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.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:

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:

  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.
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.