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
80 lines
2.4 KiB
C#
80 lines
2.4 KiB
C#
using System.Text.Json;
|
|
using Clawdforge;
|
|
using Clawdforge.Exceptions;
|
|
using Clawdforge.Models;
|
|
|
|
// Quickstart for the clawdforge C# SDK.
|
|
//
|
|
// Set environment vars:
|
|
// CLAWDFORGE_URL=http://192.168.0.5:8800
|
|
// CLAWDFORGE_TOKEN=cf_<your-app-token>
|
|
|
|
var baseUrl = Environment.GetEnvironmentVariable("CLAWDFORGE_URL")
|
|
?? "http://localhost:8800";
|
|
var token = Environment.GetEnvironmentVariable("CLAWDFORGE_TOKEN")
|
|
?? throw new InvalidOperationException("CLAWDFORGE_TOKEN env var is required");
|
|
|
|
using var client = new ForgeClient(new ForgeOptions
|
|
{
|
|
BaseUrl = baseUrl,
|
|
Token = token,
|
|
});
|
|
|
|
// 120s ceiling for the whole demo via cancellation.
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
|
|
var ct = cts.Token;
|
|
|
|
try
|
|
{
|
|
var h = await client.HealthzAsync(ct);
|
|
Console.WriteLine($"clawdforge ok={h.Ok} claude_present={h.ClaudePresent} version={h.ClaudeVersion}");
|
|
|
|
var res = await client.RunAsync(new RunRequest
|
|
{
|
|
Prompt = "Reply with JSON: {\"hello\": \"world\"}",
|
|
Model = "sonnet",
|
|
TimeoutSecs = 60,
|
|
}, ct);
|
|
|
|
Console.WriteLine($"duration: {res.DurationMs}ms stop_reason: {res.StopReason}");
|
|
|
|
// res.Result is a JsonElement — caller narrows by ValueKind.
|
|
if (res.Result.ValueKind == JsonValueKind.Object
|
|
&& res.Result.TryGetProperty("hello", out var hello))
|
|
{
|
|
Console.WriteLine($"hello: {hello.GetString()}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"raw result: {res.AsText()}");
|
|
}
|
|
|
|
// Optional file-upload demo: only runs if a path is in argv[0].
|
|
if (args.Length > 0 && File.Exists(args[0]))
|
|
{
|
|
var ft = await client.UploadFileAsync(args[0], ttlSecs: 3600, ct);
|
|
Console.WriteLine($"uploaded: token={ft.Token} size={ft.Size}");
|
|
|
|
var withFile = await client.RunAsync(new RunRequest
|
|
{
|
|
Prompt = "Describe the attached file.",
|
|
Files = new[] { ft.Token },
|
|
}, ct);
|
|
Console.WriteLine($"file run: {withFile.AsText()}");
|
|
}
|
|
}
|
|
catch (ForgeAuthException ex)
|
|
{
|
|
Console.Error.WriteLine($"auth failed: {ex.StatusCode}: {ex.Body}");
|
|
Environment.Exit(2);
|
|
}
|
|
catch (ForgeApiException ex)
|
|
{
|
|
Console.Error.WriteLine($"api error {ex.StatusCode}: {ex.Body}");
|
|
Environment.Exit(3);
|
|
}
|
|
catch (ForgeTransportException ex)
|
|
{
|
|
Console.Error.WriteLine($"transport: {ex.Message}");
|
|
Environment.Exit(4);
|
|
}
|