feat(headless): establish portable Linux host boundary

Add the presentation-free acdream-headless executable, strict no-connect configuration validation, dependency and assembly guards, and a Windows/Ubuntu CI lane that builds and tests only the portable runtime closure.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-27 01:10:45 +02:00
parent 953c469cac
commit aada8a37c1
14 changed files with 613 additions and 8 deletions

View file

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace AcDream.Headless.Configuration;
internal sealed class HeadlessConfiguration
{
[JsonRequired]
public int Version { get; init; }
[JsonRequired]
public List<HeadlessSessionDescriptor?> Sessions { get; init; } = [];
}
internal sealed class HeadlessSessionDescriptor
{
[JsonRequired]
public string Id { get; init; } = string.Empty;
}

View file

@ -0,0 +1,9 @@
namespace AcDream.Headless.Configuration;
internal sealed class HeadlessConfigurationException : Exception
{
internal HeadlessConfigurationException(string message)
: base(message)
{
}
}

View file

@ -0,0 +1,68 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AcDream.Headless.Configuration;
internal static class HeadlessConfigurationLoader
{
private const int CurrentVersion = 1;
private static readonly JsonSerializerOptions Options = new()
{
AllowTrailingCommas = false,
PropertyNameCaseInsensitive = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReadCommentHandling = JsonCommentHandling.Disallow,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};
internal static HeadlessConfiguration Load(string path)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
string fullPath = Path.GetFullPath(path);
using FileStream stream = File.OpenRead(fullPath);
HeadlessConfiguration? configuration =
JsonSerializer.Deserialize<HeadlessConfiguration>(
stream,
Options);
if (configuration is null)
{
throw new HeadlessConfigurationException(
"The configuration document is empty.");
}
if (configuration.Version != CurrentVersion)
{
throw new HeadlessConfigurationException(
$"Unsupported configuration version {configuration.Version}; "
+ $"expected {CurrentVersion}.");
}
if (configuration.Sessions is null)
{
throw new HeadlessConfigurationException(
"sessions must be an array.");
}
var sessionIds = new HashSet<string>(StringComparer.Ordinal);
foreach (HeadlessSessionDescriptor? session in configuration.Sessions)
{
if (session is null
|| string.IsNullOrWhiteSpace(session.Id))
{
throw new HeadlessConfigurationException(
"Every session requires a non-empty id.");
}
if (!sessionIds.Add(session.Id))
{
throw new HeadlessConfigurationException(
$"Duplicate session id '{session.Id}'.");
}
}
return configuration;
}
}