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.
56 lines
1.9 KiB
Swift
56 lines
1.9 KiB
Swift
// Basic example for the Clawdforge SDK.
|
|
//
|
|
// Run with: CLAWDFORGE_TOKEN=cf_... swift run ClawdforgeBasicExample
|
|
//
|
|
// This file is intentionally named `main.swift` so it can use top-level
|
|
// `await` rather than wrapping in `@main`. Swift treats a single-file
|
|
// executable target named `main.swift` as a script.
|
|
|
|
import Foundation
|
|
import Clawdforge
|
|
|
|
let baseURL = URL(string: ProcessInfo.processInfo.environment["CLAWDFORGE_URL"] ?? "http://192.168.0.5:8800")!
|
|
guard let token = ProcessInfo.processInfo.environment["CLAWDFORGE_TOKEN"] else {
|
|
FileHandle.standardError.write(Data("error: set CLAWDFORGE_TOKEN\n".utf8))
|
|
exit(1)
|
|
}
|
|
|
|
do {
|
|
let client = try ForgeClient(baseURL: baseURL, token: token)
|
|
|
|
// 1. Health check
|
|
let h = try await client.healthz()
|
|
print("claude_present: \(h.claudePresent)")
|
|
print("claude_version: \(h.claudeVersion ?? "unknown")")
|
|
|
|
// 2. Run a prompt
|
|
let res = try await client.run(RunRequest(
|
|
prompt: #"Reply with JSON: {"hello": "world"}"#,
|
|
model: "sonnet",
|
|
timeoutSecs: 60
|
|
))
|
|
print("duration: \(res.durationMs)ms")
|
|
if case .object(let dict) = res.result,
|
|
case .string(let hello) = dict["hello"] ?? .null {
|
|
print("hello = \(hello)")
|
|
} else {
|
|
print("result: \(res.result)")
|
|
}
|
|
|
|
// 3. Optional file upload + reuse — uncomment to try.
|
|
//
|
|
// let recipe = URL(fileURLWithPath: "./recipe.png")
|
|
// let ft = try await client.uploadFile(at: recipe, ttlSecs: 3600)
|
|
// let extract = try await client.run(RunRequest(
|
|
// prompt: "extract recipe data as JSON",
|
|
// files: [ft.fileToken]
|
|
// ))
|
|
// print("extract: \(extract.result)")
|
|
|
|
} catch let err as ForgeError {
|
|
FileHandle.standardError.write(Data("clawdforge error: \(err.localizedDescription)\n".utf8))
|
|
exit(2)
|
|
} catch {
|
|
FileHandle.standardError.write(Data("unexpected error: \(error)\n".utf8))
|
|
exit(2)
|
|
}
|