5 - An Aether Mod

An Aether Mod

A mod is untrusted code loaded inside a HangarBay / Aether Engine host. It can't open a socket to the broker - CasCore makes System.Net uncallable - so the host holds the permission authority and the mod asks it. The gate is the same XRUIOS.Permissions store the Manager uses for apps; a mod is just another requester id in it. The concepts are in 6 - Aether Engine Integration; this page is how to set the projects up.

There are two projects: the host (which enrols and gates mods) and the mod (untrusted, loaded under CasCore).

The host project

Lives in the Aether Engine solution, targets net8.0 (CasCore + the vendored XRUIOS.Permissions DLLs are net8.0).

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>disable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DouglasDwyer.CasCore" Version="0.3.0" />
    <!-- Transitive deps of the Pariah store that raw DLL refs don't pull. -->
    <PackageReference Include="Ceras" Version="4.1.7" />
    <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
    <PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
    <PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
    <PackageReference Include="EasyCompressor.LZ4" Version="2.1.0" />
    <PackageReference Include="K4os.Compression.LZ4" Version="1.3.8" />
    <PackageReference Include="Data.HashFunction.Blake3" Version="3.1.2" />
    <PackageReference Include="System.Data.HashFunction.Interfaces" Version="2.0.0" />
    <PackageReference Include="Standart.Hash.xxHash" Version="4.0.5" />

    <ProjectReference Include="..\Hangarbay.AetherEngine.CasSandbox\Hangarbay.AetherEngine.CasSandbox.csproj" />
    <ProjectReference Include="..\Hangarbay.AetherEngine.Security\Hangarbay.AetherEngine.Security.csproj" />

    <Reference Include="PermissionHandler"><HintPath>..\refs\PermissionHandler.dll</HintPath></Reference>
    <Reference Include="PariahCybersecurity"><HintPath>..\refs\PariahCybersecurity.dll</HintPath></Reference>
  </ItemGroup>

</Project>

Why each:

Reference Why
DouglasDwyer.CasCore the deny-by-default IL loader - the code wall
CasSandbox (project) ModRegistry, ModProfile, PariahPermissionBroker - the mod system
Security (project) SandboxFileSystem, SandboxRoot, ModId - the jail
PermissionHandler.dll Handler, AppPermissionContext - the permission store
PariahCybersecurity.dll SecureData - how keys/ids are passed to the store
Pariah transitives the store's own dependencies; the raw DLL refs don't pull them, so list them or you get a Ceras / BouncyCastle FileNotFoundException at run time

That last row is the one gotcha - a <Reference HintPath> copies the DLL but not its NuGet dependencies, so the store's transitives must be named explicitly.

The mod project

The mod references nothing except the jail type it calls, and even that is not copied (the host provides it). Targets net8.0.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>disable</Nullable>
    <AssemblyName>MyMod</AssemblyName>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Hangarbay.AetherEngine.Security\Hangarbay.AetherEngine.Security.csproj">
      <Private>false</Private>
    </ProjectReference>
  </ItemGroup>
</Project>

Private=false matters: the mod uses SandboxFileSystem's type to compile, but must load the host's copy at run time, not ship its own. Everything else a mod might reach - File, Process, sockets, sensors - it simply doesn't reference, and CasCore makes them uncallable anyway.

Registering each thing as a separate app

One ModRegistry over the host's shared store. Each mod is Enrolled by its package id (which becomes its host-assigned ModId) and granted its own capabilities. Grants to one never touch another.

// the host's ONE permission authority
var context  = new AppPermissionContext(SD(storeDir), SD(permClass), SDk(masterKey));
using var registry = new ModRegistry(handler, context, privateRoot, memoTtl: TimeSpan.FromSeconds(2));

// each row = "register this as its own app, grant it exactly these"
var mod = registry.Enroll("com.hollvania.calendarwidget");   // host-assigned ModId, never self-reported
await registry.GrantAsync(mod, "fs.read");
await registry.GrantAsync(mod, "fs.write");
await registry.GrantAsync(mod, "fs.read:shared");
await registry.GrantAsync(mod, "fs.write:shared");

// hand it its OWN jail: private dir always; the "shared" root opens only with fs.*:shared granted
var fs = registry.CreateFileSystem(mod, new[] { new SandboxRoot("shared", sharedDir) });

// live check, keyed on the mod's id (memoised so per-frame IO is cheap)
bool ok = registry.IsAllowed(mod, "fs.write:shared");

Signed vs ephemeral: Enroll(packageId) gives a stable id whose grants persist across sessions; EnrollEphemeral(label) gives a fresh id each run, so unsigned code can't bank trust. Filesystem access is itself a permission - the self dir needs fs.read/fs.write, a named root needs fs.read:<root>/fs.write:<root>.

The worked example (real, in the repo)

Hangarbay.AetherEngine.MultiModProvingGround is exactly this, built and run. It loads the default mod template (HostileMod) three times under three package ids, grants each a different set, and checks the outcomes headless:

app                              private    shared     escapes
------------------------------------------------------------------------------
com.hollvania.calendarwidget     allow      allow      blocked
com.hollvania.musicviz           allow      deny       blocked
com.evil.dataslurp               deny       deny       blocked
------------------------------------------------------------------------------
Same mod code, three registrations. Access follows the grant, not the code.

Same bytes, three identities, three access levels - and every raw File / Process / socket escape blocked by CasCore for all of them, regardless of grants. Run it with dotnet run in that project; it exits non-zero if any expectation fails, so it drops into CI next to the single-mod proving ground.

To add a distinct mod, copy the default Mod template to a new project with its own AssemblyName, stage its DLL into the host's mods/ folder (the host's StageMods target does this), and enrol it under its own package id.

Next: the fleet self-test - 6 - The Diagnostics Harness.