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