using System.Text.Json; using CareFix.Core; namespace CareFix.Agent; /// /// Runs one job locally. The agent does its own checks and does not trust the server blindly: /// only the seven known job kinds are accepted, every query must pass the same SELECT-only parser, /// limits come from the hospital's own config, and writes can only go through the guarded procedure. /// public sealed class JobRunner(AgentConfig cfg, ILogger log) { private readonly SqlGuard _guard = new(); private readonly SqlOps _ops = new( SqlOps.BuildConnectionString(cfg.SqlServer, cfg.SqlPort, cfg.Database, cfg.ReadUser, cfg.ReadPassword, cfg.Encrypt, cfg.TrustServerCertificate, "CareFix-Agent-Read"), SqlOps.BuildConnectionString(cfg.SqlServer, cfg.SqlPort, cfg.Database, cfg.WriteUser, cfg.WritePassword, cfg.Encrypt, cfg.TrustServerCertificate, "CareFix-Agent-Write"), new SqlLimits(cfg.MaxRowsPerSelect, cfg.SelectTimeoutSeconds, cfg.LockTimeoutMs)); public async Task RunAsync(AgentJob job, CancellationToken ct) { try { object? result = job.Kind switch { AgentProtocol.Kinds.Select => await SelectAsync(P(job), ct), AgentProtocol.Kinds.AnyRows => await AnyRowsAsync(P(job), ct), AgentProtocol.Kinds.GetValue => await Value(P(job), ct), AgentProtocol.Kinds.GetRow => await Row(P(job), ct), AgentProtocol.Kinds.ApplySteps => await ApplyAsync(P(job), ct), AgentProtocol.Kinds.ReadSchema => await _ops.ReadSchemaAsync(ct), AgentProtocol.Kinds.Test => await _ops.TestAsync(ct), _ => throw new OpsException(400, $"The agent does not accept job type '{job.Kind}'."), }; return new AgentResult(true, JsonSerializer.SerializeToElement(result, AgentProtocol.Json), null, null); } catch (OpsException ex) { return new AgentResult(false, null, ex.Status, ex.Message); } catch (Exception ex) when (ex is JsonException or ArgumentException or InvalidOperationException) { log.LogWarning(ex, "Job {JobId} ({Kind}) rejected", job.JobId, job.Kind); return new AgentResult(false, null, 400, "The agent could not read this request: " + ex.Message); } } private static T P(AgentJob job) => job.Payload.Deserialize(AgentProtocol.Json) ?? throw new JsonException("Empty payload."); private void Check(string sql) { var r = _guard.ValidateSelect(sql, null); if (!r.Ok) throw new OpsException(400, "Refused by the hospital agent: " + r.Reason); } private async Task SelectAsync(SelectPayload p, CancellationToken ct) { Check(p.Sql); return await _ops.RunSelectAsync(p.Sql, ct); } private async Task AnyRowsAsync(AnyRowsPayload p, CancellationToken ct) { Check(p.Sql); return await _ops.AnyRowsAsync(p.Sql, p.PkValue, ct); } private async Task Value(ValuePayload p, CancellationToken ct) => await _ops.GetValueAsync(p.Table, p.PkColumn, p.PkValue, p.Column, ct); private async Task Row(RowPayload p, CancellationToken ct) => await _ops.GetRowAsync(p.Table, p.PkColumn, p.PkValue, ct); private async Task ApplyAsync(ApplyPayload p, CancellationToken ct) { if (p.Steps.Count is 0 or > 50) throw new OpsException(400, "A change must have 1 to 50 steps."); log.LogInformation("Applying {Count} change(s) for {TicketRef}", p.Steps.Count, p.TicketRef); var n = await _ops.ApplyStepsAsync(p.Steps, p.TicketRef, ct); log.LogInformation("Applied {Count} change(s) for {TicketRef}", n, p.TicketRef); return n; } }