- Session class with try/finally pattern + Client::session(callable) block helper
- Idempotent Session::close (in-memory + server idempotent)
- __destruct best-effort close fallback (errors logged via error_log, never raised)
- Client::createSession / ::session / ::listSessions / ::getSession
- TurnResult::text() helper concatenating text events
- TurnEvent value object preserves tool_call / thinking / text uniformly
- SessionState value object for list/get responses
- ClosedSessionException for use-after-close guard
- Session::__debugInfo redacts forge back-reference (no bearer in var_dump)
- tests/SessionTest.php: 15 tests covering create/close/turn/idempotency/exception/list/state/text/destruct/regression
- README "Multi-turn / Sessions (v0.2)" section + try/finally + block-form
+ __destruct + __debugInfo notes; exception tree updated
v0.1 surface unchanged — 44 existing ClientTest tests still green, +15 new
v0.2 tests in SessionTest. Client.php diff is purely additive (285 lines
appended, zero v0.1 lines touched).
Naming follows the existing PHP SDK convention: Clawdforge\Client (not the
spec's generic Sulkta\Clawdforge\Forge), with the new types living in the
same Clawdforge\ namespace and Exception\ClosedSessionException slotted
under the existing ForgeException tree.
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
- clawdforge_session_new / _turn / _close / _list / _get
- Wraps the v0.2 session HTTP surface (POST /sessions, POST
/sessions/{id}/turn, GET/DELETE /sessions/{id}, GET /sessions)
- Tool descriptions tuned for LLM consumption: when to prefer session_new
vs run, idempotency contract on close, file_token attachment via files[]
- session_turn returns two content blocks: prose text (concat'd text events)
for direct LLM consumption + structured trace JSON (turn_index,
stop_reason, duration_ms, events) for tool-calling agents
- 404/410/auth errors from upstream surface as MCP errors with actionable
messages; no Python tracebacks leak through
- tests/test_sessions.py: 22 new tests covering the 5 tools + 404 + schema
validation + idempotent close
- tests/test_server.py: new v0.1 schema-pin regression test
- README "Sessions (v0.2)" section with example open/turn/turn/close chain
- Bump version 0.1.0 -> 0.2.0
v0.1 tools (clawdforge_healthz / _run / _upload_file) are byte-identical.
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
- Session class wrapping a session_id; block form `forge.session do |s|`
- Idempotent Session#close (in-memory short-circuit + server is also
idempotent server-side; @closed rolled back on transport failure so
retry is safe)
- Client#create_session / #list_sessions / #get_session
- TurnResult#text helper concatenates text events (skips thinking,
tool_call, and nil contents)
- Session#inspect / #to_s / #pretty_print redact the embedded client
reference so the bearer never leaks via logs / pp / irb / backtraces
- test/test_session.rb: 26 tests covering block-form auto-close on
return + on exception + with-failing-close-must-not-mask-block-error,
manual lifecycle, close idempotency (single DELETE on N closes),
transport-failure rollback, turn round-trip + files/timeout/auth,
closed-session guard, empty-prompt guard, TurnResult#text edge cases,
list/get state, cross-token 404, server 410, redaction in inspect /
to_s / PP, and a v0.1 #run regression smoke
v0.1 surface (#run, #upload_file, #create_token, #list_tokens,
#revoke_token, #healthz) is byte-identical — purely additive.
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
- Session class with Symbol.asyncDispose for `await using` ergonomics (ES2024)
- forge.session({ agent }) preferred form; forge.createSession() explicit
- forge.listSessions() / forge.getSession()
- TurnResult / TurnEvent / SessionState types + turnText() helper
- Idempotent Session.close() (200 on re-close server-side)
- tests/sessions.test.ts: 13 tests covering disposal/idempotency/throw/list/state/404/regression
- README "Multi-turn / Sessions (v0.2)" section + fallback try/finally docs
tsconfig.json: lib bumped to add ES2023 + ESNext.Disposable so the
Symbol.asyncDispose / AsyncDisposable types resolve under TS 5.9. Target
stays ES2022 — the disposable runtime hooks are TS-erasable, no runtime
polyfill needed; consumers just need Node 20.4+ at runtime to use the
`await using` form (documented in the README; the v0.1 surface and the
explicit createSession + try/finally fallback continue to work on Node 18+).
package.json: bumped to 0.2.0; engines.node stays >=18 since the v0.1
surface is unchanged. v0.1 /run path unchanged (regression test added).
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
- Session class wrapping a clawdforge session_id; context-manager auto-close
- forge.session(agent=...) block form (preferred)
- forge.create_session() / forge.list_sessions() / forge.get_session() admin shapes
- TurnResult dataclass with .text() helper concatenating text events
- Idempotent Session.close() — safe in finally / __exit__
- tests/test_sessions.py: 16 tests covering block/manual/idempotency/exception/list/state/text-helper/v0.1-regression
- README "Multi-turn / Sessions (v0.2)" section
- pyproject version 0.1.0 -> 0.2.0; package __version__ matches
Architecture: matches the existing v0.1 client — sync, requests-based,
single Forge-owned requests.Session for connection pooling. Session holds
a back-reference to the Forge for HTTP work (no per-Session HTTP client).
This mirrors how Forge already exposes its own context-manager pattern, so
nothing about the threading/lifecycle story changes for callers.
v0.1 /run path unchanged — 49 existing tests still green, +16 new tests
for v0.2 (target was 9; covered the spec's 9 plus extras for empty-prompt
local validation, include_closed=false param, empty-id ValueError, and
s.state() round-trip).
mypy --strict src/clawdforge/ clean.
Spec: memory/spec-clawdforge-v0.2.md
Server core: 940861f
- Dockerfile: install acpx@latest alongside @anthropic-ai/claude-code
- compose.yml: bind /mnt/user/appdata/clawdforge/acpx-sessions:/root/.acpx/sessions
- DB: additive sessions + session_events tables in store.py SCHEMA
- clawdforge/acpx_runner.py: AcpxManager + AcpxSession, bounded async pool,
per-invocation subprocess model (acpx CLI itself owns the queue-owner
lifecycle, so each turn = one fresh `acpx prompt -s <uuid>` call)
- server.py: POST/GET/DELETE /sessions + POST /sessions/{id}/turn + GET /sessions
- Per-app isolation: 404 (not 403) on cross-token session access — no
existence leak across tokens
- Lifespan-managed TTL sweeper: every 60s soft-closes idle sessions past
CLAWDFORGE_SESSION_TTL_SECS (1h default), hard-deletes ledger rows past
CLAWDFORGE_SESSION_HARD_TTL_SECS (24h default)
- session_events audit table parallel to existing runs table
(events: create, turn, close, sweep_close, hard_delete)
- /healthz now reports acpx_present + acpx_version + open_sessions count
- tests/test_sessions.py: 16 tests covering create/turn/close/list/isolation/
sweep/pool-full/regression. /run regression test asserts byte-identical
v0.1 response shape.
ACPX research notes (v0.6.1, openclaw/acpx):
- npm package is `acpx`, not `@openclaw/acpx`
- Sessions are scoped by (agentCommand, cwd, name?). We mint our own UUID
as `--name` and give every session a unique cwd subdir, so the scope key
is collision-free across apps.
- session_id source: ours. We pass --name <uuid>, ACPX records it under
~/.acpx/sessions/<encoded-id>.json. We never need to parse ACPX's
acpxRecordId — our UUID is canonical.
- Subprocess lifetime: per-invocation, NOT per-session. The acpx CLI itself
spawns/maintains a per-session "queue owner" process via local IPC; each
`acpx prompt` call we make either elects itself owner or enqueues. The
AcpxSession class is therefore a thin (uuid, cwd, asyncio.Lock) handle,
not a long-lived stdio pipe. The spec's "owns one stdio pipe pair" model
was rewritten to match reality — flagged here.
- Close semantics: soft-close via `acpx sessions close <name>`. The
on-disk record stays (ACPX's `sessions prune` is the hard-delete path,
not invoked from clawdforge). DELETE /sessions/<id> is documented as
idempotent (200 with already_closed=true on second call) so SDKs can
call close() in finally/Drop blocks safely.
- File uploads: ACPX has no file-attach ACP method exposed via the CLI.
We prepend a [Attached files] header listing absolute paths; the agent
uses its Read tool to open them. Same behavior as /run --files in v0.1.
- Permissions: --approve-all on the turn invocation since the container is
unattended and callers are bearer-token-trusted. Future v0.3 may expose
a per-session permission policy.
/run endpoint unchanged — backwards compat verified by
test_run_endpoint_unchanged + test_run_endpoint_unchanged_error_shape.
Spec: memory/spec-clawdforge-v0.2.md
ACPX CLI ref: https://github.com/openclaw/acpx/blob/main/docs/CLI.md
HIGH:
- H1: nlohmann::json::exception wrapped as ProtocolError at 5 sites in
client.cpp via with_protocol_guard helper. Preserves the documented
clawdforge::Error catch-all base contract; nlohmann types never leak
into the message (e.what() only).
- H2: libcurl MAXREDIRS=5, REDIR_PROTOCOLS_STR="http,https"
(CURLOPT_REDIR_PROTOCOLS bitmask fallback for libcurl < 7.85.0),
UNRESTRICTED_AUTH=0L. Defense-in-depth on top of libcurl's automatic
bearer strip on cross-host redirects (>=7.64.0).
MEDIUM:
- M1: upload_file resolves the path via std::filesystem::canonical up
front. Closes broken-symlink, symlink-loop, and TOCTOU-on-target
classes without a doc burden on callers.
- M2: README "Linking" section documents the public-ABI nlohmann_json
implication. v0.2 wrapper deferred.
- M3: README "Threat model" section documents the parse-depth concern
on the result field of /run replies. Runtime guard skipped for v0.1
per audit recommendation (low yield, complexity).
LOW:
- L1: cxx_std_20 → cxx_std_17 in CMakeLists.txt (no C++20-only
features in the library source; broader downstream reach). Examples
and tests still build via designated initializers (g++ accepts these
in C++17 mode).
- L2: RunResult struct doc clarifies that missing ok/duration_ms
decode to defaults — opt-out forward-compat.
- L3: Client class doc clarifies that moved-from instances must not
have any non-special-member methods invoked (UB), with explicit
callout on base_url() returning an internal reference.
Test-only:
- cpp-httplib 0.15.3 → 0.20.1. Optional backends (OpenSSL / zlib /
brotli / zstd) forced off to keep the dep graph minimal. Test-only,
never on the consumer wire path. README "Test deps" section added
for transparency.
Tests added (12 → 23 cases, 70 → 106 assertions):
- protocol_error on malformed response for healthz, run, upload_file,
create_token, list_tokens (H1 regression)
- redirect_clamp_test (H2 regression — TransportError after 5+ hops)
- redirect_protocol_clamp (H2 regression — ftp:// Location rejected)
- upload_file_canonicalize: symlink→file works, broken symlink
rejected, symlink loop rejected, directory rejected (M1 regression)
Verified:
- cmake --build build clean (-Wall -Wextra -Wpedantic -Wshadow
-Wconversion -Wsign-conversion -Wold-style-cast -Werror)
- ctest --output-on-failure all green (Release)
- ASan + UBSan: 23/23 cases, 106/106 assertions, zero diagnostics
Audit: memory/clawdforge-audits/cpp-bae34a7.md
MEDIUM:
- M1: JsonSerializerOptions.MaxDepth = 32 on the consolidated
JsonDefaults.Options (referenced from both ForgeClient and
RunResult.AsJson<T>) so the result payload's arbitrary upstream JSON
cannot stack-walk the runtime.
- M2: JsonDocumentOptions.MaxDepth = 32 in SummarizeBody for parsing
error-body summaries — defensive belt alongside the existing 8 MiB
body cap.
- M3: UploadStreamAsync doc updated to match reality — the input stream
IS disposed when the request completes (matches HttpClient /
MultipartFormDataContent / StreamContent convention). Old doc was
incorrect; chose doc-update over a non-disposing wrapper to stay
closest to standard .NET stream semantics.
LOW:
- L2: RunResult.AsJson<T>() now guards JsonValueKind.Undefined and
returns default(T) instead of throwing InvalidOperationException
(e.g. when RunResult is constructed without a server payload).
- L4: IsNullOrWhiteSpace consistent across RunRequest.Prompt,
CreateTokenRequest.Name, RevokeTokenAsync.name, UploadFileAsync.path,
UploadStreamAsync.fileName (was IsNullOrEmpty letting space through).
Nit polish:
- BaseUrl cached in ctor instead of rebuilt per access.
- JsonDefaults moved to its own file (Models/JsonDefaults.cs) and is
now the single source of truth for serializer options across the
client.
- examples/Basic/Program.cs comment fixed: '60s' → '120s' to match
TimeSpan.FromSeconds(120).
README:
- HTTPS / WireGuard recommendation in the Notes section — SDK does not
enforce HTTPS, callers off-LAN should tunnel.
- .NET 8.0.10+ runtime recommendation with cref to CVE-2024-30105 and
CVE-2024-43485 (SDK does not exercise the affected code paths;
belt-and-suspenders).
- UploadStream section reflects the corrected disposal contract.
Tests (12 → 19, all passing):
- JsonOpts_MaxDepth_RejectsDeeplyNested — 200-deep result rejected via
ForgeTransportException wrapping JsonException, no stack overflow.
- SummarizeBody_DeeplyNestedHandled — 200-deep error body still
produces ForgeAuthException with raw body intact; summary parse
fails closed without crashing.
- UploadStreamAsync_DisposesCallerStream — DisposeObservingStream
helper verifies the contract change.
- AsJson_OnUndefinedResult_DefaultReturned — reference + value type.
- RunRequest_PromptWithOnlyWhitespace_Rejected.
- CreateToken_NameWithOnlyWhitespace_Rejected.
- BaseUrl_Cached_ReusesString — Assert.Same identity check.
Build: dotnet build -c Release -m:1 clean (0 warnings, 0 errors).
Tests: dotnet test -c Release -m:1 → 19 passed, 0 failed.
Pack: dotnet pack -c Release -o dist -m:1 clean.
Vulns: dotnet list package --vulnerable --include-transitive → 0.
Audit: memory/clawdforge-audits/csharp-09aca58.md
HIGH:
- H1: __debugInfo() redacts token on Client + AppToken; #[\SensitiveParameter]
on Client constructor's $token param so PHP scrubs it from stack traces.
MEDIUM:
- M1: uploadStream(StreamInterface, filename, ttl) overload so callers
handling form uploads have a non-path entry point. README warning above
the API table on uploadFile path-trust.
- M2: RunRequest now rejects empty-string model/system in the constructor
(callers should pass null/omit rather than '' to use defaults).
- M3: new MalformedResponseException extends ForgeException for
"transport succeeded, body unparseable as expected JSON object". Decoupled
from ApiException so callers can distinguish "server told me no" from
"server replied 200 with garbage". README + ApiException docstring updated.
- M4: non-UTF-8 / malformed JSON now flows through M3's new exception.
- M5: ApiException error-message extraction falls back to json_encode
(capped at 200 chars) when the error field is an object/array, so
callers don't get empty messages on {"error":{"code":...,"msg":...}}.
LOW:
- L2: revokeToken now requires server response ok === true, raises
MalformedResponseException on missing/false ok rather than silently
returning true.
- L5: README WordPress snippet uses bare Client (matches the use line above).
- L7: 29 new tests — token redaction (3), uploadStream (2), empty
model/system (2), MalformedResponseException across 7 scenarios incl.
non-UTF-8, ApiException object-error formatting + 200-char cap, revoke
ok=true requirement + ok=false + empty-name, RunRequest timeout bounds
(3) + non-string/empty files entries (2), uploadFile unreadable-path
+ 4xx + 5xx, healthz 500, Authorization header asserted on every
endpoint.
README polish: TLS verify=false caveat under "Custom HTTP client".
Audit memo: memory/clawdforge-audits/php-1cff9b8.md
P1 (release blocker):
- multipart now RFC 7578 compliant (was injecting bare LF before file
content via Swift """...""" multi-line literals; corrupted binary
uploads — PNG/PDF/JPEG). Body now built via explicit "\r\n"
concatenation so every byte on the wire is auditable.
P2:
- CustomStringConvertible redacts token on ForgeClient + AppToken
(default mirror was leaking plaintext via print / String(reflecting:)
/ SwiftUI string interpolation).
- revokeToken now pre-validates name against ^[a-z0-9_-]{1,64}$ and
rejects path-traversal sequences with ForgeError.invalidArgument
before percent-encoding (urlPathAllowed left /, +, ;, =, ,, @
unescaped).
- baseURL with non-empty path/query/fragment rejected at construct.
init is now `throws` — host-only URLs only, since the SDK builds
request URLs by string concatenation.
P3:
- Fixed misleading "custom encoding" comment on RunRequest (it's just
Optional + JSONEncoder default behavior).
- public init on RunFailure (was decode-only).
- Task.checkCancellation() inside the multipart chunk loop — multi-GB
uploads now abort promptly when the parent Task is cancelled.
- 0o600 perms on the staged temp upload file (was inheriting umask,
typically 0o644 — unwanted in multi-tenant /tmp).
- Documented JSONValue.number Double precision limit (loses precision
for ints > 2^53).
Tests:
- testMultipartIsCRLFCompliant: writes a PNG-signature payload, scans
the captured body for the `\r\n\n` bare-LF pattern AND verifies the
bytes after `Content-Type: image/png\r\n\r\n` match the payload
exactly.
- testForgeClientDescriptionRedactsToken
- testAppTokenDescriptionRedactsToken (covers both nil and non-nil
token cases)
- testRevokeTokenRejectsTraversalName: foo/../bar, FOO, spaces, +, ;,
=, @, 65-char names, empty
- testBaseURLWithPathRejected: /api, /v1, ?query, #fragment; host-only
variants still accepted
- testRunFailurePublicInit
- testTempFilePerms: scans /tmp during the in-flight upload to verify
the staged clawdforge-upload-* file is 0o600
- Existing tests updated for the now-throwing init.
README + Examples updated for the throwing init.
Audit: memory/clawdforge-audits/swift-e4e8192.md
Note: untested locally — Swift toolchain not present in this sandbox.
Needs `swift build -c release` + `swift test` verification on a Swift
5.9+ host (macOS or Linux) before tagging the next release.
Security:
- S1: bearer via tmpfile/--config, not cmdline arg (no /proc/<pid>/cmdline leak)
- S2/S3: JSON-escape user input in --files, --ip-cidrs, token name
- S4: URL-encode token name in revoke
- S5: refuse to source cf.env unless 0600/0400 + owner-matched
- S6: reject ; in upload paths to defeat curl @ filename injection
Correctness:
- B1: refuse cf run - on TTY stdin
- B2: replace fragile files splice with proper JSON-array composer (raw: passthrough in _json_obj_from_assoc)
- B3: disable glob on comma-split (set -f around loop)
- B4: only create stdin tmpfile when actually used
- B5: EXIT trap (was RETURN; missed _die exit)
- B6/B7: --max-time + stderr capture on uploads
- B8: drop bare Bearer header on healthz when no token
- B9: validate admin subcommand before token
- B10: wire _extract_error into HTTP-error path
- U3: dedicated '# --- end help ---' sentinel for cmd_help
New: clients/bash/test/test_cf.sh (curl wrapper mock + 23 assertions covering
all of the above; fully shellcheck-clean).
Audit: memory/clawdforge-audits/bash-347fdde.md
Async Kotlin/JVM client built on Ktor + kotlinx.serialization. Every I/O
method is a `suspend` function; the client is `Closeable` for `use { }`.
Sealed `ForgeException` hierarchy enables exhaustive `when` over auth,
run-failure, generic-API, and transport errors. Models use `@SerialName`
to bridge idiomatic camelCase Kotlin properties to the snake_case wire
format. `RunResult.result` is a `JsonElement` so callers can narrow with
the standard `kotlinx.serialization.json` extensions.
- Kotlin 1.9.25 / JVM 17 toolchain
- Ktor 2.3.12 client (CIO engine; pluggable via ForgeOptions.engine)
- kotlinx-serialization 1.6.3, kotlinx-coroutines 1.8.1
- 14 tests (JUnit 5 + Ktor MockEngine), all green
- `./gradlew build` clean, `publishToMavenLocal` works
- MIT license declared in publishing block
Mirrors the surface of the Go and Rust SDKs (healthz, run, uploadFile,
admin tokens CRUD).
Modern C++20 SDK targeting CMake 3.20+. Library is RAII / move-only,
backed by a libcurl easy handle per Client. Public surface is throwing;
exception hierarchy under clawdforge::Error covers AuthError, APIError
(carries status_code + body), TransportError, and ProtocolError.
Dependencies: libcurl + nlohmann/json (FetchContent or find_package).
Tests use cpp-httplib's in-process server + doctest. 12 test cases /
70 assertions cover healthz, run with JSON / text / 502 / files,
multipart upload, full token CRUD, transport failure, URL normalization,
and bad-input rejection. Clean under -Wall -Wextra -Wpedantic -Werror,
ASan + UBSan clean (no leaks, no UB).
upload_file streams via curl_mime_filedata — no in-memory buffering.
Install path produces clawdforge::clawdforge target consumable via
target_link_libraries; FetchContent path mirrors the existing Rust /
Go SDK ergonomics. MIT licensed.
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
Drops a Model Context Protocol server into clients/mcp/ that wraps the
clawdforge HTTP surface so MCP-aware clients (Claude Desktop, Claude Code,
Cursor, Zed, custom agents) can call it as a native tool — claude talking
to claude through the LAN bridge.
Three tools exposed:
- clawdforge_healthz -> GET /healthz
- clawdforge_run -> POST /run
- clawdforge_upload_file -> POST /files
Admin endpoints intentionally NOT exposed; token minting stays human-gated.
Implementation notes:
- Built on the official `mcp` Python SDK (>=1.0). asyncio-native server,
stdio transport, low-level Server class with @list_tools / @call_tool
handlers.
- Self-contained `requests` HTTP wrapper rather than depending on the
sibling clients/python SDK — keeps clawdforge-mcp installable
standalone. Same error taxonomy (ForgeError / ForgeAPIError /
ForgeAuthError / ForgeTransportError).
- Sync HTTP calls offloaded via asyncio.to_thread so a slow `claude -p`
can't stall the MCP event loop.
- Errors are formatted into a single 'clawdforge error: ...' text block
with isError=True; tracebacks never leak through the JSON-RPC pipe.
- Logging goes to stderr (CLAWDFORGE_MCP_LOG=DEBUG to enable). stdout
is reserved for JSON-RPC framing.
- Config via env: CLAWDFORGE_URL (default http://localhost:8800) and
CLAWDFORGE_TOKEN (required). MCP clients pass these via their `env`
config block.
Tests: 12 unit tests covering tool discovery, healthz, run-success,
run-with-files, run-empty-prompt, run-subprocess-502, run-auth-401,
upload happy path, upload missing file, unknown tool, server factory.
HTTP layer mocked via `responses`. Plus a manual end-to-end stdio
smoke (initialize + tools/list round-trip) verified during build.
Includes ready-to-paste Claude Desktop and Claude Code config examples,
and a README documenting install, env, all three tools, and operational
notes (stdout-is-sacred, error wrapping, no streaming).
Idiomatic Go client wrapping the FastAPI surface in server.py — Healthz,
Run, UploadFile/UploadReader, and admin token CRUD. stdlib net/http only,
context-first signatures, typed errors (ErrAuth sentinel, RunFailure for
/run 502s, APIError for other 4xx/5xx, TransportError for network/EOF).
RunResult.Result is captured as json.RawMessage and materialized via
.AsJSON(out) / .AsText() because claude returns either parsed JSON or
plain text depending on prompt. UploadFile streams via io.Pipe + multipart
without buffering the file in memory.
Module: gitea.sulkta.com/Sulkta-Coop/clawdforge/clients/go
Includes cmd/cf-cli demo binary and httptest-based test suite (13 tests).
Sync requests-based SDK in clients/python/. Wraps /healthz, /run, /files,
and /admin/tokens behind a Forge class with typed exceptions
(ForgeError + Transport/API/Auth subclasses) and dataclass response shapes
(RunResult, FileToken, AppToken). HTTP timeout = run timeout + 30s margin,
matching the pattern cauldron has been running inline. No retries —
caller's job since /run isn't idempotent.
24 unit tests via responses, all passing. Install with
pip install -e clients/python/.
Tiny curl wrapper so cron jobs, deploy scripts, and shell pipes can drive
clawdforge without dragging in Python or Go.
Surface mirrors the server:
cf healthz
cf run "<prompt>" [--model] [--system] [--timeout] [--files t1,t2]
cf run - # prompt via stdin (long prompts)
cf upload <path> [--ttl 3600]
cf admin token-mint <name> [--ip-cidrs cidr1,cidr2]
cf admin token-list
cf admin token-revoke <name>
Configuration via env or ~/.config/clawdforge/cf.env:
CLAWDFORGE_URL, CLAWDFORGE_TOKEN, CLAWDFORGE_ADMIN_TOKEN
Output: JSON to stdout (pipe to jq freely), errors to stderr,
exit codes 0/1/2/3/4/5 mapping clearly to transport/usage/auth/4xx/5xx.
No deps beyond curl + POSIX tools. jq is optional (only used for prettier
error output if available).
Smoke-tested against live clawdforge on Lucy: healthz green, /run with
small prompt returns parsed JSON in 2-7s, /run with stdin large prompts
relies on clawdforge's server-side stdin path (>64KB), admin token-list
returns the cauldron token row.
Build/install:
sudo install -m 755 clients/bash/cf /usr/local/bin/cf
Cobb's seed-cleanup job hit OSError [Errno 7] Argument list too long with
a 577KB prompt. Linux ARG_MAX is typically 128KB-2MB depending on kernel +
env; passing the full prompt as 'claude -p <PROMPT>' fails for big jobs.
Fix: detect prompt size > 64KB threshold, omit the positional prompt
argument from the CLI invocation and pipe via subprocess.run(input=...)
instead. claude -p reads the prompt from stdin when no positional given.
System prompt + flags still pass as CLI args (those stay small).
LAN-only HTTP service that runs claude -p subprocess on behalf of Sulkta apps.
Bearer token + IP allowlist gated. SQLite-backed token registry + run audit log.
- POST /run run a prompt, return parsed result
- POST /files upload a file, get a file_token to attach to /run
- POST /admin/tokens mint per-app tokens (admin-bootstrap-token gated)
- GET /admin/tokens list, DELETE /admin/tokens/<name> revoke
- GET /healthz liveness + claude --version smoke
Container = node:22 + npm-installed @anthropic-ai/claude-code + uvicorn/FastAPI
wrapper. Persistent volumes for /data (sqlite + run staging) and /root/.claude
(subscription auth — survives container rebuilds; auth via 'docker exec -it
clawdforge claude /login' once). Compose binds 192.168.0.5:8800 only — no
public proxy.
First consumer = cauldron (about to land).