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
255 lines
7.9 KiB
Markdown
255 lines
7.9 KiB
Markdown
# 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 <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:
|
|
|
|
```cmake
|
|
find_package(PkgConfig REQUIRED)
|
|
pkg_check_modules(CLAWDFORGE REQUIRED IMPORTED_TARGET clawdforge)
|
|
target_link_libraries(my_app PRIVATE PkgConfig::CLAWDFORGE)
|
|
```
|
|
|
|
## Quickstart
|
|
|
|
```c
|
|
#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.
|
|
|
|
```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.
|