Files
LithosAnanke/src/starkernel/capsule/capsule_birth.c
T
Robert Allan JamesandClaude Sonnet 5 3e0966095c Stadium: real VM-patron admission + real COOL dispatch at KILL (FABRIC-3.md §B)
capsule_vm_kill() had zero Stadium involvement (vm_cleanup()/sf_free()
only), and child-VM birth only ever called stadium_grant_quota() -- a
resource pool for the VM's own future word/block patrons, never
stadium_admit() for the VM itself. stadium_birth_hera() looked like a
precedent but admits Hera into her own quota as a permanently pinned
cell 0, which can never reach stadium_evict() -- not a working example
of COOL firing for a VM.

Adds size_t stadium_patron_cell to VMRegistryEntry. At birth, right
after the existing stadium_grant_quota() call, admits a candidate into
the new VM's own quota mirroring stadium_birth_hera()'s shape
(identity=0, mass=1, behaviour=COOL) but deliberately unpinned --
pinning would need a new "unpin" primitive (none exists) to ever evict
it later, and unpinned costs nothing since nothing wires COOL's
dispatch body to kill anything; the worst case of an unrelated natural
eviction is stale bookkeeping, tolerated the same way
stadium_word_forget() already tolerates staleness elsewhere. At
capsule_vm_kill() and capsule_vm_kill_all_nonmama(): stadium_evict()
the tracked cell if still resident, silently tolerating refusal
(already gone). stadium_dispatch()'s COOL case needed no new payload
body -- same as it already is for words, where COOL has no defined
extra action beyond stadium_evict()'s own universal reservoir credit.

On investigation this turned out not to be entangled with the
still-iterating Tripod/Zuse/messaging vision after all -- birth and
kill already funnel through two single choke points, so the earlier
deferral (previous commit) was overcautious.

Verified live: a second, new "Stadium: dispatch cell=... behaviour=
COOL" now fires immediately before every PARITY:KILL line, for both
Hermes and Artemis, distinct from the pre-existing COMMON-CH
word-eviction self-test's own COOL print. Conservation
(resident_sum + reservoir == Q48_ONE) intact throughout. Clean
zero-warning compile and clean boot on all three architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
2026-08-26 02:53:56 -04:00

635 lines
22 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
*/
/**
* capsule_birth.c - VM Birth Protocol Implementation (M7.1)
*
* Mama init, baby birth, and experiment execution.
* Freestanding - no libc dependency.
*
* Birth sequence for a baby VM:
* 1. Find capsule by name (capsule_find_by_name)
* 2. Assert CAPSULE_BIRTH_ELIGIBLE
* 3. Validate content hash
* 4. vm_alloc_hook() — fresh VM
* 5. vm_exec_hook(payload) — IDENTITY (init capsule from Hera's store)
* 6. vm_exec_hook("1 LOAD") — PERSONALITY (baby's personal init.4th from block 1, if present)
* 7. Log parity record
*/
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_run.h"
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include "starkernel/vm/stadium.h" /* item 4.1a -- stadium_grant_quota() */
#include "vm.h"
#include "platform_alloc.h"
/*===========================================================================
* VM Execution Hooks
*===========================================================================*/
static CapsuleExecFn vm_exec_fn = 0;
static CapsuleDictHashFn vm_dict_hash_fn = 0;
static CapsuleVMAllocFn vm_alloc_fn = 0;
void capsule_birth_set_hooks(
CapsuleExecFn exec_fn,
CapsuleDictHashFn dict_hash_fn,
CapsuleVMAllocFn vm_alloc_fn_arg)
{
vm_exec_fn = exec_fn;
vm_dict_hash_fn = dict_hash_fn;
vm_alloc_fn = vm_alloc_fn_arg;
}
/*===========================================================================
* VM Registry — dynamic linked list, heap-allocated via kmalloc
*===========================================================================*/
typedef struct vm_node {
VMRegistryEntry entry;
struct vm_node *next;
} vm_node_t;
static vm_node_t *vm_registry_head = (void *)0;
static uint32_t vm_registry_count = 0;
/* item 3.8: vm_id generation moved to vm_uuid_next()'s deterministic pool;
* the monotonic next_vm_id counter this replaced is gone. Hera's id is
* vm_uuid_hera() (fixed, reserved), not drawn from the pool. */
/* Copy at most VM_NAME_MAX-1 chars, always null-terminate */
static void vm_name_copy(char *dst, const char *src) {
uint32_t i;
for (i = 0; i < (VM_NAME_MAX - 1u) && src[i]; i++)
dst[i] = src[i];
dst[i] = '\0';
}
/* Case-sensitive equality test (no libc) */
static int vm_name_eq(const char *a, const char *b) {
while (*a && *b) {
if (*a != *b) return 0;
a++; b++;
}
return *a == *b;
}
/* Internal: return mutable pointer into registry node for vm_id */
static VMRegistryEntry *vm_find_entry_ptr(VMUuid vm_id) {
vm_node_t *node = vm_registry_head;
while (node) {
if (vm_uuid_equal(node->entry.vm_id, vm_id)) return &node->entry;
node = node->next;
}
return (void *)0;
}
void capsule_vm_registry_init(void *mama_vm_ptr) {
uint32_t i;
vm_node_t *node;
vm_node_t *next;
vm_node_t *mama;
/* Free any nodes from a previous init (defensive) */
node = vm_registry_head;
while (node) {
next = node->next;
kfree(node);
node = next;
}
vm_registry_head = (void *)0;
vm_registry_count = 0;
/* Mama is always the reserved, fixed vm_uuid_hera() id (item 3.8) */
mama = (vm_node_t *)kmalloc(sizeof(vm_node_t));
if (!mama) return;
mama->entry.vm_id = vm_uuid_hera();
mama->entry.state = VM_STATE_LIVE;
mama->entry.birth_capsule_id = 0;
mama->entry.birth_timestamp_ns = 0;
mama->entry.birth_dict_hash = 0;
mama->entry.flags = 0;
mama->entry.parent_vm_id = vm_uuid_hera(); /* self-referential: Hera is the root */
mama->entry.vm_ptr = mama_vm_ptr;
mama->entry.stadium_patron_cell = STADIUM_CELL_NONE; /* set for real by
* stadium_birth_hera()
* separately -- not
* tracked here */
for (i = 0; i < VM_NAME_MAX; i++) mama->entry.name[i] = '\0';
vm_name_copy(mama->entry.name, "Hera");
mama->next = (void *)0;
vm_registry_head = mama;
vm_registry_count = 1;
/* From this point on all console output is prefixed [Hera] */
console_set_vm_name("Hera");
}
static VMRegistryEntry *vm_registry_alloc(void) {
uint32_t i;
vm_node_t *node;
vm_node_t *tail;
node = (vm_node_t *)kmalloc(sizeof(vm_node_t));
if (!node) return (void *)0;
node->entry.vm_id = vm_uuid_none(); /* not yet assigned -- NOT
* vm_uuid_hera(): a
* newly-allocated embryo
* is never Hera (item
* 3.8 caught this exact
* collision class again) */
node->entry.state = VM_STATE_EMBRYO;
node->entry.birth_capsule_id = 0;
node->entry.birth_timestamp_ns = 0;
node->entry.birth_dict_hash = 0;
node->entry.flags = 0;
node->entry.parent_vm_id = vm_uuid_hera(); /* only Hera calls BIRTH
* today; see design doc's "explicitly
* out of scope" for making this dynamic */
node->entry.vm_ptr = (void *)0;
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
for (i = 0; i < VM_NAME_MAX; i++) node->entry.name[i] = '\0';
node->next = (void *)0;
/* Append to tail */
if (!vm_registry_head) {
vm_registry_head = node;
} else {
tail = vm_registry_head;
while (tail->next) tail = tail->next;
tail->next = node;
}
vm_registry_count++;
return &node->entry;
}
int capsule_vm_registry_get(VMUuid vm_id, VMRegistryEntry *out) {
VMRegistryEntry *entry;
if (!out) return -1;
entry = vm_find_entry_ptr(vm_id);
if (!entry) return -1;
*out = *entry;
return 0;
}
uint32_t capsule_vm_registry_count(void) {
return vm_registry_count;
}
/* Live population, distinct from vm_registry_count above: vm_registry_count
* is monotonic (incremented on every vm_registry_alloc(), never decremented
* on death), so it counts every VM ever born, not the outer Stadium's
* current occupancy. FABRIC.md item 1.5's bound is on LIVE VMs -- a dead or
* stillborn slot doesn't hold Stadium capacity, and gating on the monotonic
* total would mean the fleet could never regrow after any VM's death,
* which contradicts Hera's own kill-then-rebirth lifecycle (TRIPOD-TEST's
* "K soak" check, capsule_vm_physics.c). Not exposed in the public header:
* only capsule_birth_baby's bound check needs it today. */
static uint32_t vm_registry_live_count(void) {
vm_node_t *node = vm_registry_head;
uint32_t live = 0;
while (node) {
if (node->entry.state == VM_STATE_LIVE) live++;
node = node->next;
}
return live;
}
int capsule_vm_find_by_name(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
if (!name || !out) return -1;
node = vm_registry_head;
while (node) {
if (vm_name_eq(node->entry.name, name)) {
*out = node->entry;
return 0;
}
node = node->next;
}
return -1;
}
/* Fold ASCII letter to lowercase (no libc) */
static char vm_to_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
/* Case-insensitive ASCII equality (no libc) */
static int vm_name_eq_nocase(const char *a, const char *b) {
while (*a && *b) {
if (vm_to_lower(*a) != vm_to_lower(*b)) return 0;
a++; b++;
}
return *a == *b;
}
int capsule_vm_find_by_name_nocase(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
if (!name || !out) return -1;
node = vm_registry_head;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
*out = node->entry;
return 0;
}
node = node->next;
}
return -1;
}
void capsule_vm_set_state(VMUuid vm_id, uint32_t state) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (entry) entry->state = state;
}
void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
VMRegistryEntry *entry;
if (!name) return;
entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
vm_name_copy(entry->name, name);
}
/*===========================================================================
* Internal: init.4th dispatch (PERSONALITY layer)
*
* After a baby VM runs its identity capsule, attempt to execute block 1.
* Block 1 is the PERSONALITY layer — the baby's personal init.4th.
* Failure is silent: the block may not exist, which is normal.
*===========================================================================*/
static void dispatch_init_forth(void *vm_ctx) {
/* M9: run baby's personal init.4th from block 1 once per-VM block
* storage is isolated. Until then this is a no-op to avoid executing
* Mama's block 1 content on every child VM. */
(void)vm_ctx;
}
/*===========================================================================
* VM Kill
*===========================================================================*/
int capsule_vm_kill(const char *name) {
VMRegistryEntry *entry;
VM *vm;
VMUuid vm_id;
uint32_t i;
if (!name) return -1;
/* Locate by name (case-insensitive) */
{
vm_node_t *node = vm_registry_head;
entry = (VMRegistryEntry *)0;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
entry = &node->entry;
break;
}
node = node->next;
}
}
if (!entry) {
console_puts("KILL: ");
console_puts(name);
console_println(" not found");
return -1;
}
/* Hera cannot be killed */
if (vm_uuid_is_hera(entry->vm_id)) {
console_println("KILL: cannot kill Hera");
return -1;
}
/* Already dead — idempotent */
if (entry->state == VM_STATE_DEAD) {
console_puts("KILL: ");
console_puts(name);
console_println(" already dead");
return 0;
}
vm_id = entry->vm_id;
vm = (VM *)entry->vm_ptr;
/* FABRIC-3.md SS B, VM-COOL: reap this VM's own Stadium patron cell for
* real, dispatching COOL. Refusal (already naturally reclaimed by
* unrelated quota pressure, or never admitted) is silently tolerated --
* KILL tears the VM down unconditionally either way. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
entry->stadium_patron_cell = STADIUM_CELL_NONE;
}
/* Tear down and free */
if (vm) {
vm_cleanup(vm);
sf_free(vm);
}
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
for (i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
capsule_parity_log_kill(vm_id, name);
console_puts("KILL: ");
console_puts(name);
console_println(" dead");
return 0;
}
void capsule_vm_kill_all_nonmama(void) {
vm_node_t *node;
VM *vm;
VMUuid vm_id;
node = vm_registry_head;
while (node) {
if (vm_uuid_is_hera(node->entry.vm_id) || node->entry.state == VM_STATE_DEAD) {
node = node->next;
continue;
}
vm_id = node->entry.vm_id;
vm = (VM *)node->entry.vm_ptr;
if (node->entry.stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(node->entry.stadium_patron_cell);
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
}
if (vm) {
vm->halted = 1;
vm_cleanup(vm);
sf_free(vm);
}
node->entry.vm_ptr = (void *)0;
node->entry.state = VM_STATE_DEAD;
capsule_parity_log_kill(vm_id, node->entry.name);
node = node->next;
}
}
/*===========================================================================
* Mama Init
*===========================================================================*/
CapsuleRunResult capsule_birth_mama(
void *mama_vm,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena)
{
if (!mama_vm || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn)
return CAPSULE_RUN_ERR_INVALID;
const CapsuleDesc *mama_cap = capsule_find_mama_init(dir, descs);
if (!mama_cap) return CAPSULE_RUN_ERR_INVALID;
CapsuleValidateResult vr = capsule_validate(mama_cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
uint64_t pre_dict_hash = vm_dict_hash_fn(mama_vm);
(void)pre_dict_hash;
const uint8_t *payload = capsule_get_payload(mama_cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID;
int exec_result = vm_exec_fn(mama_vm, (const char *)payload, mama_cap->length);
if (exec_result != 0) return CAPSULE_RUN_ERR_EXEC_FAIL;
uint64_t post_dict_hash = vm_dict_hash_fn(mama_vm);
capsule_parity_log_mama_init(
mama_cap->capsule_id,
mama_cap->content_hash,
post_dict_hash);
{
VMRegistryEntry *mama_entry = vm_find_entry_ptr(vm_uuid_hera());
if (mama_entry) {
mama_entry->birth_capsule_id = mama_cap->capsule_id;
mama_entry->birth_dict_hash = post_dict_hash;
}
}
/* item 3.8: seed the vm_uuid pool now that the Mama capsule's content
* hash is known -- before any baby birth (none happens today, item 0.1),
* so the same capsule booted twice produces the same id sequence. */
vm_uuid_pool_init(mama_cap->content_hash);
return CAPSULE_RUN_OK;
}
/*===========================================================================
* Baby Birth
*===========================================================================*/
CapsuleRunResult capsule_birth_baby(
const char *capsule_name,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
VMUuid *out_vm_id,
void **out_vm_ctx)
{
if (!capsule_name || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn || !vm_alloc_fn)
return CAPSULE_RUN_ERR_INVALID;
/* Locate by name */
const CapsuleDesc *cap = capsule_find_by_name(dir, descs, names, capsule_name);
if (!cap) return CAPSULE_RUN_ERR_INVALID;
if (!CAPSULE_BIRTH_ELIGIBLE(cap->flags)) return CAPSULE_RUN_ERR_NOT_ELIGIBLE;
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
if (vm_registry_live_count() >= stadium_max_vm_count()) {
capsule_parity_log_birth_failed(vm_uuid_none(), cap->capsule_id,
CAPSULE_RUN_ERR_FLEET_FULL, 0);
return CAPSULE_RUN_ERR_FLEET_FULL;
}
VMRegistryEntry *entry = vm_registry_alloc();
if (!entry) return CAPSULE_RUN_ERR_INVALID;
VMUuid vm_id = vm_uuid_next();
entry->vm_id = vm_id;
entry->state = VM_STATE_EMBRYO;
entry->birth_capsule_id = cap->capsule_id;
/* Allocate baby VM */
void *new_vm = vm_alloc_fn();
entry->vm_ptr = new_vm;
if (!new_vm) {
entry->state = VM_STATE_STILLBORN;
capsule_parity_log_birth_failed(vm_id, cap->capsule_id,
CAPSULE_RUN_ERR_STILLBORN, 0);
return CAPSULE_RUN_ERR_STILLBORN;
}
/* item 4.2: set before the IDENTITY exec below, so any word this baby
* dispatches during her own init capsule already attributes heat to her
* own reservoir, not vm_uuid_hera()'s (item 4.1's hardcoded default). */
((VM *)new_vm)->stadium_vm_id = vm_id;
/* item 4.6 fix (FABRIC-2.md, 2026-08-18): granted here, before IDENTITY
* exec, not after a confirmed live birth as item 4.1a originally placed
* it. item 4.1a's placement assumed no VM's own IDENTITY code would ever
* need a Stadium quota before birth completes -- true until item 4.6's
* Artemis capsule started auto-running a block-admission stress campaign
* as part of her own init.4th load. Without a quota yet, every
* STADIUM-ADMIT during that campaign refused unconditionally (quota
* slot < 0), 100% of trials, on all three architectures. Trade-off this
* introduces: a VM that dies stillborn below (IDENTITY exec fails) has
* still consumed half of Hera's free list, with no rollback -- accepted
* because stadium_grant_quota() failure was already non-fatal and a
* stillbirth here is the rare case, not the common one. */
(void)stadium_grant_quota(vm_id, vm_uuid_hera());
/* FABRIC-3.md SS B, VM-COOL: admit this VM as a patron of its own
* quota -- identity 0 (same convention stadium_birth_hera() uses for
* "patron zero"), heat 0 (no reservoir cost), unpinned (unlike Hera --
* there is no unpin primitive, and pinning here would make the
* explicit KILL-time eviction below unreachable without adding one).
* Unpinned means unrelated quota pressure on this VM's own words/
* blocks could naturally evict this cell before KILL ever runs; that
* is tolerated, not a bug -- nothing wires COOL's dispatch body to
* kill anything, so the only visible effect is entry->stadium_
* patron_cell going stale, which the KILL-time eviction below already
* tolerates (stadium_evict() simply refuses if it's already gone).
* Soft failure, same as stadium_grant_quota() above -- a refused
* admission leaves stadium_patron_cell at STADIUM_CELL_NONE, and
* nothing downstream depends on it succeeding. */
{
StadiumPatronHeader vm_patron;
uint8_t *raw = (uint8_t *)&vm_patron;
size_t i;
for (i = 0; i < sizeof(vm_patron); i++) raw[i] = 0;
vm_patron.identity = 0;
vm_patron.heat = 0;
vm_patron.ttl = 0;
vm_patron.link = 0;
vm_patron.contains = STADIUM_CONTAINS_NONE;
vm_patron.mass = 1;
vm_patron.flags = 0;
vm_patron.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
entry->stadium_patron_cell = stadium_admit(vm_id, &vm_patron);
}
const uint8_t *payload = capsule_get_payload(cap, arena);
if (!payload) {
entry->state = VM_STATE_STILLBORN;
capsule_parity_log_birth_failed(vm_id, cap->capsule_id,
CAPSULE_RUN_ERR_INVALID, 0);
return CAPSULE_RUN_ERR_INVALID;
}
/* IDENTITY: run init capsule */
int exec_result = vm_exec_fn(new_vm, (const char *)payload, cap->length);
if (exec_result != 0) {
uint64_t partial_hash = vm_dict_hash_fn(new_vm);
entry->state = VM_STATE_STILLBORN;
entry->birth_dict_hash = partial_hash;
capsule_parity_log_birth_failed(vm_id, cap->capsule_id,
CAPSULE_RUN_ERR_EXEC_FAIL, partial_hash);
return CAPSULE_RUN_ERR_EXEC_FAIL;
}
/* PERSONALITY: per-VM block storage is M9 scope; no-op until then */
dispatch_init_forth(new_vm);
uint64_t dict_hash = vm_dict_hash_fn(new_vm);
entry->state = VM_STATE_LIVE;
entry->birth_dict_hash = dict_hash;
capsule_parity_log_birth(vm_id, cap->capsule_id, cap->content_hash, dict_hash);
if (out_vm_id) *out_vm_id = vm_id;
if (out_vm_ctx) *out_vm_ctx = new_vm;
return CAPSULE_RUN_OK;
}
/*===========================================================================
* Experiment Execution
*===========================================================================*/
CapsuleRunResult capsule_run_experiment(
void *mama_vm,
const char *capsule_name,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
uint64_t *out_run_id)
{
if (!mama_vm || !capsule_name || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn)
return CAPSULE_RUN_ERR_INVALID;
const CapsuleDesc *cap = capsule_find_by_name(dir, descs, names, capsule_name);
if (!cap) return CAPSULE_RUN_ERR_INVALID;
if (!CAPSULE_DOE_ELIGIBLE(cap->flags)) return CAPSULE_RUN_ERR_NOT_ELIGIBLE;
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
uint64_t pre_dict_hash = vm_dict_hash_fn(mama_vm);
const uint8_t *payload = capsule_get_payload(cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID;
int exec_result = vm_exec_fn(mama_vm, (const char *)payload, cap->length);
uint64_t post_dict_hash = vm_dict_hash_fn(mama_vm);
CapsuleRunRecord record;
record.run_id = 0;
record.vm_id = vm_uuid_hera(); /* experiments run on Mama's own VM */
record.reserved = 0;
record.capsule_id = cap->capsule_id;
record.capsule_hash = cap->content_hash;
record.pre_dict_hash = pre_dict_hash;
record.post_dict_hash = post_dict_hash;
record.started_ns = 0;
record.ended_ns = 0;
record.result_code = (exec_result == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
record.flags = cap->flags;
uint64_t run_id = capsule_run_log_record(&record);
capsule_parity_log_run(vm_uuid_hera(), run_id, cap->capsule_id, pre_dict_hash, post_dict_hash);
if (out_run_id) *out_run_id = run_id;
return (exec_result == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
}