Use Warden

C integration

Choose the right C API, compile policies in memory, launch workloads and retrieve sockets and receipts.

Choisir le bon niveau d’API

  • libmaelys-executor.a reçoit un maelys_executor_plan_t déjà résolu. C’est l’API mécanique utilisée par l’adaptateur launcher de mcp-runtime. C'est aussi le niveau qui expose les cibles OCI, TrustedRunSpec, le planner et les backends Apple Container et Krun. Un backend est le moteur d’isolation qui applique la décision ; le tableau des backends décrit ce que chacun sait appliquer.
  • libmaelys-warden.a accepte une policy inline, du JSON source ou du MIR et compose maelys-sandbox-policy, Executor et, si nécessaire, Egress.

Une maelys_warden_config_t nouvellement créée n’est pas une policy vide. Elle a des valeurs par défaut (frontend=auto, environnement hérité, stdio capturé), mais il faut encore lui fournir exactement une commande et une source de policy avant maelys_warden_spawn().

Le diagramme suivant montre la commande et les trois sources de policy alternatives qui alimentent une configuration, puis le chemin qu’elles suivent jusqu’au backend et au handle d’exécution.

Le JSON, le MIR et la policy inline sont trois entrées alternatives vers la même décision. Ils ne sont jamais transmis tels quels au workload.

Construire une policy inline

L’API C reflète exactement run --ro|--rw, sans chaîne de profil. Chaque appel inline a donc une ligne de commande équivalente, qui compile vers la même décision.

Les chemins relatifs sont ancrés dans le workspace, les chemins absolus sont des chemins hôte explicites, et les règles sont récursives. Ajouter une destination réseau sélectionne automatiquement le réseau médié.

Policy inline : API C

CODE
result = maelys_warden_config_set_inline_policy(
    config, MAELYS_WARDEN_WORKSPACE_READ_ONLY, &error);
if (result == MAELYS_EXECUTOR_OK)
    result = maelys_warden_config_add_fs_access(
        config, MAELYS_WARDEN_FS_WRITE, "build", &error);
if (result == MAELYS_EXECUTOR_OK)
    result = maelys_warden_config_add_network_destination(
        config, "github.com", 443, &error);

Policy inline : équivalent CLI

Cette invocation produit exactement la même décision compilée que les trois appels C ci-dessus.

CODE
maelys-warden run --ro --allow-write build --net github.com:443 -- COMMAND

Réseau ambiant : API C

Pour autoriser explicitement le réseau IP ambiant au lieu du réseau médié :

CODE
result = maelys_warden_config_set_direct_network(config, &error);

Réseau ambiant : équivalent CLI

Le mot-clé direct doit constituer la valeur entière de l’option ; il ne se combine pas avec des destinations médiées.

CODE
maelys-warden run --ro --net direct -- COMMAND

Garde-fous des méthodes add_*

Les méthodes add_* exigent une policy inline déjà sélectionnée. Elles sont refusées avec une source JSON ou MIR, ce qui interdit d’élargir par accident un artefact externe ou scellé.

À quoi sert set_policy_json ?

maelys_warden_config_set_policy_json() copie un document JSON source v2 dans la configuration. Lors de encode_mir() ou spawn(), libmaelys-mir :

  1. parse et valide strictement ce JSON ;
  2. normalise, trie et déduplique la décision ;
  3. produit un MIR v2 canonique ;
  4. Sandbox résout ses racines symboliques (workspace, temp, minimal-runtime) contre le host courant ;
  5. Executor sélectionne un backend capable d’appliquer tout le plan.

Il ne s’agit donc ni d’une configuration arbitraire d’Executor ni de JSON transmis au workload. Par exemple :

CODE
static const char policy_json[] =
    "{\"formatVersion\":2,"
    "\"filesystem\":{\"default\":\"deny\",\"rules\":["
      "{\"access\":\"read\",\"path\":{\"special\":\"minimal-runtime\"}},"
      "{\"access\":\"read\",\"path\":{\"root\":\"workspace\","
        "\"relative\":\"\"},\"scope\":\"tree\"},"
      "{\"access\":\"write\",\"path\":{\"root\":\"temp\","
        "\"relative\":\"\"},\"scope\":\"tree\"}]},"
    "\"network\":{\"mode\":\"none\"},"
    "\"process\":{\"treeConfinement\":\"required\"}}";

result = maelys_warden_config_set_policy_json(
    config, (const uint8_t *)policy_json, strlen(policy_json), &error);

La mémoire source peut être libérée dès le retour : les octets sont copiés. Pour conserver le résultat compilé sans lancer de processus :

CODE
uint8_t *mir = NULL;
size_t mir_size = 0;
result = maelys_warden_config_encode_mir(config, &mir, &mir_size, &error);
/* écrire mir si nécessaire */
maelys_warden_bytes_free(mir);

Exemple complet minimal

Le programme suivant utilise la policy JSON précédente, exécute /usr/bin/true, attend sa fin et récupère son receipt, la preuve d’exécution qui enregistre la décision retenue et son issue. Les schémas et leurs champs sont détaillés sur la page receipts d’exécution.

CODE
#include <maelys/warden.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

static const char policy_json[] =
    "{\"formatVersion\":2,"
    "\"filesystem\":{\"default\":\"deny\",\"rules\":["
      "{\"access\":\"read\",\"path\":{\"special\":\"minimal-runtime\"}},"
      "{\"access\":\"read\",\"path\":{\"root\":\"workspace\","
        "\"relative\":\"\"},\"scope\":\"tree\"},"
      "{\"access\":\"write\",\"path\":{\"root\":\"temp\","
        "\"relative\":\"\"},\"scope\":\"tree\"}]},"
    "\"network\":{\"mode\":\"none\"},"
    "\"process\":{\"treeConfinement\":\"required\"}}";

int main(void) {
    const char *child[] = {"/usr/bin/true"};
    maelys_warden_config_t *config = NULL;
    maelys_warden_execution_t *execution = NULL;
    maelys_execution_status_t status = {0};
    char *error = NULL;
    char *receipt = NULL;

    maelys_executor_result_t result =
        maelys_warden_config_create(&config, &error);
    if (result == MAELYS_EXECUTOR_OK)
        result = maelys_warden_config_set_argv(config, 1, child, &error);
    if (result == MAELYS_EXECUTOR_OK)
        result = maelys_warden_config_set_policy_json(
            config, (const uint8_t *)policy_json,
            strlen(policy_json), &error);
    if (result == MAELYS_EXECUTOR_OK)
        result = maelys_warden_spawn(config, &execution, &error);

    while (result == MAELYS_EXECUTOR_OK && !status.exited)
        result = maelys_warden_execution_wait(execution, 100, &status, &error);
    if (result == MAELYS_EXECUTOR_OK)
        result = maelys_warden_execution_receipt_json(
            execution, &receipt, &error);

    if (receipt) fputs(receipt, stdout);
    else fprintf(stderr, "%s\n", error ? error : "execution failed");

    maelys_warden_string_free(receipt);
    if (execution) (void)maelys_warden_execution_destroy(execution, NULL);
    maelys_warden_config_destroy(config);
    maelys_executor_error_free(error);
    if (result != MAELYS_EXECUTOR_OK) return 1;
    return status.term_signal ? 128 + status.term_signal : status.exit_code;
}

Dans cet exemple, aucun frontend réseau n’est choisi explicitement : la valeur est auto, mais la policy demande network:none. Aucun fd 4, proxy, Egress ou relay n’est donc créé et le receipt porte frontend:"none".

Le véritable exemple compilé, qui gère aussi les pipes capturés, est examples/c/run_inline.c.

Le diagramme suivant retrace les échanges entre l’application C, la bibliothèque et le workload, de config_create() jusqu’à execution_destroy().

Cible OCI Apple Container ou Krun

La façade maelys_warden_config_t et la CLI construisent une cible locale ou OCI avec le même planner TrustedRunSpec ABI 8. Le diagramme suivant montre les quatre entrées de ce RunSpec et les candidats OCI qu’il peut sélectionner.

La requête doit appliquer exactement le même TargetSpec que le RunSpec, utiliser un environnement vide ou explicite et ne pas demander ISOLATED_DUPLEX. Le candidat simple refuse les règles filesystem portables ; le composite accepte seulement READ/WRITE récursif avec ERROR/SKIP. Le chemin médié livré est le proxy Egress à travers UDS/vsock, pas le frontend workload fd 4.

Le guide Apple Container contient la séquence C et le tableau des backends donne les masques exacts. Le test tests/test_apple_container_e2e.c est le consommateur complet de référence pour prepare, spawn, lifecycle et receipt v3.

Sur Apple Silicon, le paquet Krun optionnel expose les deux inscriptions publiques suivantes. create_default() les appelle automatiquement quand le driver et son bundle sont présents ; un intégrateur à chemins explicites peut les enregistrer lui-même :

CODE
maelys_executor_register_krun(executor, driver_path, &error);
maelys_executor_register_krun_bubblewrap(executor, driver_path, &error);

La première refuse toute règle filesystem portable. La seconde ajoute uniquement READ/WRITE récursif, exécuté par le Bubblewrap Warden lié au seal de l'artefact OCI. Les deux refusent une plateforme autre que linux/arm64. Ils acceptent network:none et le frontend proxy médié : le relay invité scellé rejoint le Egress propre à l'exécution sur AF_VSOCK 4520. Le frontend fd 4 reste refusé à travers la frontière de noyau.

Avec la façade produit, l'appelant ne construit pas manuellement les quatre handles RunSpec :

CODE
result = maelys_warden_config_set_oci_target(
    config,
    "ghcr.io/maelys/tool",
    "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "linux/amd64",
    "/workspace",
    "/workspace",
    &error);
if (result == MAELYS_EXECUTOR_OK)
    result = maelys_warden_config_set_receipt_schema(config, 3u, &error);
/* Option avancée et fail-closed : aucun fallback si ce candidat échoue. */
if (result == MAELYS_EXECUTOR_OK)
    result = maelys_warden_config_set_backend(
        config, "apple-container-bubblewrap", &error);

L'image et le digest sont deux champs séparés dans l'API C pour éviter tout parsing ambigu. Le digest résolu est obligatoire, la plateforme n'est jamais déduite d'un tag et une cible OCI exige le receipt v3.

Qu’est-ce que maelys-relay-bootstrap ?

Le relay/bootstrap est un petit exécutable livré par Executor. Il n’est utilisé que sous Linux lorsque la policy est network:mediated et que le frontend est proxy.

Le diagramme suivant situe le relay entre un workload non modifié et le Egress privé à l’exécution, et montre pourquoi une connexion IP directe est refusée.

Il lance le relay, attend sa readiness puis démarre le workload. Il ne contient ni allowlist, ni credential, ni moteur proxy : le principal est lié à l'endpoint AF_UNIX privé et la décision reste dans Egress.

Il faut distinguer le fichier et ses instances. Le fichier exécutable est installé une seule fois et bind-monté en lecture seule dans chaque sandbox. À chaque spawn médié, Executor lance un bootstrap/superviseur privé, qui crée un processus relay privé pour cette exécution puis le workload. Ce relay unique multiplexe toutes les connexions du workload ; il n’y a ni daemon relay global, ni processus relay par connexion. Le serveur Egress, son thread, son socket AF_UNIX et son principal lié à l'endpoint sont eux aussi propres à l’exécution.

Le relay n’est ni une bibliothèque chargée par le workload, ni le fd 4. Ses fd sont des détails internes : listener TCP loopback, connexions acceptées, flux AF_UNIX vers Egress et readiness. Le frontend fd4 décrit plus bas est un chemin alternatif pour un programme Maelys-aware ; aucun relay n’est lancé dans ce mode.

Le diagramme suivant récapitule les trois chemins possibles d’une policy network:mediated, selon le frontend choisi et la plateforme.

Le chemin est automatique. libmaelys-warden cherche, dans cet ordre :

  1. MAELYS_WARDEN_GUEST_BUNDLE si l’exploitant désigne un bundle invité ;
  2. MAELYS_RELAY_BOOTSTRAP si l’exploitant l’a défini ;
  3. à côté de l’exécutable appelant puis dans son ../libexec sous Linux ;
  4. /usr/local/libexec/maelys-warden/guest/linux-arm64/maelys-relay-bootstrap puis son équivalent /opt/homebrew/libexec/… ;
  5. /usr/local/libexec/maelys-relay-bootstrap ;
  6. /usr/libexec/maelys-relay-bootstrap.

Un programme normal ne doit donc pas appeler maelys_warden_config_set_relay_bootstrap(). L’override explicite demeure utile pour un bundle hermétique, un conteneur, des tests ou une installation sous un préfixe non conventionnel. Le chemin doit être absolu, canonique, exécutable, non modifiable par groupe/autrui et appartenir à root ou à l’utilisateur.

Receipt d’exécution

maelys_warden_execution_receipt_json() renvoie une chaîne JSON UTF-8 dans le schéma choisi sur la configuration. Le défaut local reste maelys.execution-receipt/v1. Un appel à maelys_warden_config_set_receipt_schema(config, 3, &error) sélectionne maelys.execution-receipt/v3; une cible OCI le sélectionne automatiquement et refuse tout autre schéma. v3 ajoute le TargetSpec, la décision complète du planner, les preuves de plateforme native ou traduite et les couches d'isolation ordonnées. Les deux formats contiennent l’identifiant d’invocation, les digests de décision et de plan, le médiateur, le backend/version, le début, la tentative d’arrêt forcé et l’issue (exitCode ou termSignal). Aucun ne contient de credential Egress, secret d’environnement ou chemin de socket privé.

L’appel est possible juste après spawn() : le snapshot aura completed:false. Après wait(), il contient l’issue finale. La bibliothèque retourne la chaîne en mémoire ; l’appelant peut l’écrire où il veut. Le CLI --receipt FILE attend la fin puis effectue une écriture atomique en mode 0600. Il n’est pas possible d’écrire le receipt final avant l’exécution, puisque le backend, les timestamps et l’issue n’existent pas encore.

Le format local historique est illustré dans docs/examples/execution-receipt.json et défini par docs/schemas/execution-receipt-v1.schema.json. Les quatre variantes v3 (simple/composite, native/traduite) se trouvent dans docs/examples/execution-receipt-v3-*.json ; leur schéma normatif est docs/schemas/execution-receipt-v3.schema.json.

Pour une cible locale, la base d’environnement par défaut est l’environnement hôte. maelys_warden_config_set_oci_target() remplace ce défaut non touché par un environnement vide : une VM ne reçoit jamais silencieusement les variables ambiantes macOS. Un appel explicite à INHERIT est refusé pour OCI. Utilisez maelys_warden_config_add_environment() pour copier uniquement les variables revues ; leurs valeurs restent en mémoire et ne passent pas par argv.

La façade conserve des délais bornés adaptés au domaine : 1000/1000/1000 ms pour un processus local et 180000/2000/3000 ms pour spawn/grace/force d’une cible OCI froide. Une intégration ayant son propre SLO peut les remplacer avec maelys_warden_config_set_timeouts() ; zéro est refusé, donc cette API ne crée jamais une attente infinie.

Workload Maelys-aware sur fd 4

Pour une policy network:mediated avec le frontend fd4, le fd 4 du workload est un canal de contrôle Unix datagram. Ce n’est pas le socket Internet. Le workload demande host:port; le broker de l’adaptateur appelle le connecteur natif Egress. Egress applique l’allowlist scellée et garde le pair du flux relayé ; le workload reçoit l’autre extrémité par SCM_RIGHTS.

Client fd 4 : API C

CODE
#include <maelys/netclient.h>

int socket_fd = -1;
char *error = NULL;
maelys_netclient_result_t result = maelys_netclient_connect(
    MAELYS_NETCLIENT_DEFAULT_CHANNEL_FD, /* 4 */
    "example.com", 80, &socket_fd, &error);
if (result == MAELYS_NETCLIENT_OK) {
    /* socket_fd est un flux TCP relayé, bloquant et CLOEXEC. */
}

Client fd 4 : lancement CLI

examples/c/fd4_client.c est un workload complet : il obtient ce socket, envoie une requête HTTP et copie la réponse. Après make examples, la CLI lance le binaire compilé sous la policy correspondante.

CODE
maelys-warden run \
  --policy examples/policies/example-http-fd4.json \
  --network-frontend fd4 \
  -- build/release/bin/example-fd4-client example.com 80

Limites du canal fd 4

Une demande pour un autre host ou port retourne MAELYS_NETCLIENT_ERR_DENIED. Les appels concurrents partageant le même fd 4 doivent être sérialisés. Executor n’interprète aucun paquet et n’ouvre aucune connexion : libmaelys-netclient parle au broker de libmaelys_executor_egress_adapter, lequel délègue toute décision et tout relay à Egress. Le détail de SCM_RIGHTS, ses garanties et ses limites est dans docs/egress-bridge.md.

Édition de liens statiques

Un consommateur C se lie statiquement en une seule ligne, sans énumérer les bibliothèques à la main.

CODE
cc app.c $(pkg-config --cflags --libs --static maelys-warden) -o app

pkg-config fournit l’ordre complet : Run, adaptateurs Sandbox/Egress, Executor, Sandbox/MIR, Egress, System et pthread. make examples compile des consommateurs réels dans examples/c/ et protège cette ligne de lien.