namespace marlin.agents.shell; using System.Net.Http.Json; using core.contracts; using core.models; using Grpc.Core; using Grpc.Net.Client; using ProtoBuf.Grpc.Client; public class MarlinApiClient { private readonly string _baseUrl; private readonly Guid _agentId; private readonly string _agentToken; private readonly HttpClient _http; public MarlinApiClient(string baseUrl, Guid agentId, string agentToken) { this._baseUrl = baseUrl; this._agentId = agentId; this._agentToken = agentToken; _http = new HttpClient { BaseAddress = new Uri(this._baseUrl) }; _http.DefaultRequestHeaders.Add("Agent", $"{this._agentId} {this._agentToken}"); } public static MarlinApiClient FromEnvironment() { var baseUrl = RequireEnv("MARLIN_API_BASE_URL"); var agentId = RequireEnv("MARLIN_AGENT_ID"); var agentToken = RequireEnv("MARLIN_AGENT_TOKEN"); return new MarlinApiClient(baseUrl, Guid.Parse(agentId), agentToken); } private static string RequireEnv(string name) => Environment.GetEnvironmentVariable(name) ?? throw new InvalidOperationException($"Missing required environment variable: {name}"); public async Task Handshake(AgentStatus status, CancellationToken ct = default) { var response = await _http.PostAsJsonAsync("api/dispatch/handshake", status, ct); response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync(ct))!; } public async Task QueueNextJob(CancellationToken ct = default) => await _http.GetFromJsonAsync("api/dispatch/queue", ct); public GrpcChannel CreateJobScopedRpc(DispatchJob job) { var credentials = CallCredentials.FromInterceptor( async (_, metadata) => { metadata.Add("Agent", $"{this._agentId} {this._agentToken}"); metadata.Add("Job", $"{job.Id} {job.Token}"); }); var channel = GrpcChannel.ForAddress(this._baseUrl, new GrpcChannelOptions { Credentials = ChannelCredentials.Create(new SslCredentials(), credentials) }); return channel; } }