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.areçoit unmaelys_executor_plan_tdéjà résolu. C’est l’API mécanique utilisée par l’adaptateur launcher demcp-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.aaccepte une policy inline, du JSON source ou du MIR et composemaelys-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
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.
maelys-warden run --ro --allow-write build --net github.com:443 -- COMMANDRéseau ambiant : API C
Pour autoriser explicitement le réseau IP ambiant au lieu du réseau médié :
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.
maelys-warden run --ro --net direct -- COMMANDGarde-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 :
- parse et valide strictement ce JSON ;
- normalise, trie et déduplique la décision ;
- produit un MIR v2 canonique ;
- Sandbox résout ses racines symboliques (
workspace,temp,minimal-runtime) contre le host courant ; - 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 :
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 :
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.
#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 :
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 :
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 :
MAELYS_WARDEN_GUEST_BUNDLEsi l’exploitant désigne un bundle invité ;MAELYS_RELAY_BOOTSTRAPsi l’exploitant l’a défini ;- à côté de l’exécutable appelant puis dans son
../libexecsous Linux ; /usr/local/libexec/maelys-warden/guest/linux-arm64/maelys-relay-bootstrappuis son équivalent/opt/homebrew/libexec/…;/usr/local/libexec/maelys-relay-bootstrap;/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
#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.
maelys-warden run \
--policy examples/policies/example-http-fd4.json \
--network-frontend fd4 \
-- build/release/bin/example-fd4-client example.com 80Limites 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.
cc app.c $(pkg-config --cflags --libs --static maelys-warden) -o apppkg-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.