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