Workflow YAML reference
This is the full reference for a merobox workflow file: the top-level schema, how to declare nodes, and the complete catalogue of step types. For how a workflow runs (lifecycle, variable capture, retries, parallelism), see the workflow engine.
Every field documented here is grounded in the schema merobox actually
validates (merobox/commands/bootstrap/config.py) and the step implementations
(merobox/commands/bootstrap/steps/). Validate a file before running it:
merobox bootstrap run workflow.yml --dry-runTop-level schema
Section titled “Top-level schema”A workflow is a mapping with a name, a node declaration, and a list of steps.
name: My Workflowdescription: What this workflow proves
nodes: count: 2 image: ghcr.io/calimero-network/merod:prerelease prefix: calimero-node
steps: - type: wait seconds: 5| Key | Required | Type | Meaning |
|---|---|---|---|
name |
yes | string | Workflow name (shown in output). |
description |
no | string | Free-text description. |
nodes |
one of nodes/remote_nodes |
object | Local Docker/binary node config (see Node configuration). |
remote_nodes |
one of nodes/remote_nodes |
map | Externally-running nodes addressed by URL (see Remote nodes). |
steps |
no | list | Ordered list of steps to run. |
nuke_on_start |
no | bool (false) |
Delete all node data before the run. |
nuke_on_end |
no | bool (false) |
Delete all node data after the run. |
stop_all_nodes |
no | bool (false) |
Stop all nodes at the end (otherwise they keep running). |
restart |
no | bool (false) |
Stop then start nodes at the beginning instead of reusing running ones. |
wait_timeout |
no | int (60) |
Default timeout (seconds) for waiting operations. |
force_pull_image |
no | bool (false) |
Re-pull Docker images before the run (Docker mode only). |
auth_service |
no | bool (false) |
Run a mero-auth container behind a Traefik proxy (Docker mode only). |
auth_image |
no | string | Custom auth-service image. |
auth_use_cached |
no | bool (false) |
Use a cached auth frontend. |
webui_use_cached |
no | bool (false) |
Use a cached WebUI frontend. |
auth_mode |
no | string | Node auth mode, e.g. embedded, enabling login/refresh/ws_connect steps. |
log_level |
no | string (debug) |
Log level applied to nodes. |
rust_backtrace |
no | string (0) |
RUST_BACKTRACE value for nodes. |
no_docker |
no | bool (false) |
Run nodes as native merod binaries instead of containers. |
binary_path |
no | string | Path to the merod binary for no_docker mode. |
e2e_mode |
no | bool (false) |
Clear merod’s default public bootstrap list and assign a private per-workflow rendezvous namespace. |
preserve_default_bootstrap |
no | bool (false) |
Under e2e_mode, keep merod’s default boot-node list instead of clearing it. |
bootstrap_nodes |
no | list | Explicit bootstrap peers to connect to. |
topology |
no | object | Startup wiring: bootstrap (discovery only) or nat (relay). See Topology. |
Node configuration
Section titled “Node configuration”Count-based mode
Section titled “Count-based mode”The quickest form: ask for N identical nodes and let merobox name and port them.
nodes: count: 3 prefix: calimero-node # nodes become calimero-node-1, -2, -3 image: ghcr.io/calimero-network/merod:prerelease base_port: 2428 # first node's server port (increments per node) base_rpc_port: 2528 # first node's RPC port (increments per node)| Field | Required | Type | Meaning |
|---|---|---|---|
count |
yes (this mode) | int ≥ 1 | Number of nodes to create. |
prefix |
no | string (calimero-node) |
Name prefix; nodes are <prefix>-<n>. |
image |
no | string | Docker image for the nodes. |
base_port |
no | int | Server port of the first node (increments). |
base_rpc_port |
no | int | RPC port of the first node (increments). |
config_path |
no | string | Shared config.toml for all nodes (skips init). |
use_image_entrypoint |
no | bool (false) |
Use the Docker image’s entrypoint. |
mdns |
no | bool | Force discovery.mdns on/off (set false to exercise the rendezvous/relay path). |
network_admin |
no | bool (true) |
Add NET_ADMIN capability so inject_network_fault works. |
Explicit mode
Section titled “Explicit mode”Name each node and give it per-node settings. Use this when nodes need distinct ports, config, or a mock-TEE boot.
nodes: tee-owner: port: 7080 rpc_port: 7180 tee-replica: port: 7081 rpc_port: 7181 mock_tee: true # boot as `merod run --mock-tee`Per-node keys include port, rpc_port, config_path (per-node override), and
mock_tee. Explicit mode is mutually exclusive with count.
Remote nodes
Section titled “Remote nodes”Address already-running nodes by URL instead of managing containers. A workflow
with only remote_nodes runs in remote-only mode (no local node lifecycle).
remote_nodes: prod-node-1: url: https://node1.example.com description: Production node 1 auth: method: api_key # none | api_key | password api_key: ${NODE1_API_KEY}| Field | Required | Type | Meaning |
|---|---|---|---|
url |
yes | string | Node base URL. |
auth.method |
no | none|api_key|password |
Authentication method. |
auth.username / auth.password |
no | string | Credentials for password. |
auth.api_key / auth.key |
no | string | API key for api_key. |
description |
no | string | Human label. |
Topology
Section titled “Topology”Setting topology replaces the default startup wiring. By default merobox puts
every node on one bridge, writes all of its siblings into each node’s
bootstrap.nodes, and leaves mDNS on — convenient, and unlike any deployment,
where a node is told about one or two bootstrap peers, has to ask one of them who
else exists, and has no multicast path to peers on other hosts.
Two variants:
type: bootstrap — one plain bridge, one boot-node, mDNS forced off, and a
single bootstrap address per client. Clients can only learn a sibling’s address
by asking the boot-node, so this exercises bootstrap → identify → kad →
rendezvous and nothing else. Use it when discovery itself is what you want to
test: a regression there fails the scenario and means only that.
topology: type: bootstrap boot_node: image: ghcr.io/calimero-network/boot-node:edge # optionaltype: nat — a boot-node on a public bridge, a NAT gateway, and clients on an
--internal LAN bridge with no direct path to each other. Required to exercise
relay-reservation recovery and DCUtR hole-punching. It also exercises the
discovery path above, which is why a discovery regression can surface here as a
NAT failure; prefer bootstrap when the relay is not the point.
topology: type: nat nat_mode: cone # cone | symmetric boot_node: image: merobox/boot-node:local # optional; auto-built when omitted keypair: ./boot-node-key.json # optional; fresh keypair when omittedBoth variants require Docker mode, force discovery.mdns = false on every
client, and skip the sibling bootstrap wiring. nodes: must use the count:
form. Unknown keys under topology are rejected rather than ignored, so a
nat_mode left behind on a bootstrap block fails validation instead of
silently doing nothing.
Common step keys
Section titled “Common step keys”Every step is a mapping with a type. These keys are available on all steps:
| Field | Required | Type | Meaning |
|---|---|---|---|
type |
yes | string | The step type (one of the catalogue below). |
name |
no | string | Human-readable label shown in output. |
outputs |
no | map | Capture response values into workflow variables — see capturing outputs. |
expected_failure |
no | bool (false) |
Pass only if the operation is refused. A step that unexpectedly succeeds fails. |
expected_error |
no | string | Pass only if the recorded error contains this substring (case-sensitive), {{placeholders}} resolved first. Requires expected_failure: true. |
Without expected_error, expected_failure: true accepts any error at all,
including the node being unreachable standing in for the refusal under test.
Pin the reason whenever the workflow is gating a specific refusal:
- type: upgrade_group node: calimero-node-1 group_id: '{{group_id}}' target_application_id: '{{app_v2}}' expected_failure: true expected_error: 'identity downgrade forbidden'The call step is the one exception to the unexpected-success rule: it keeps
warning and passing, because workflows use expected_failure on a read as a
soft “may not have propagated yet” probe and rely on its None error exports.
Fields specific to each step are documented per type. Placeholder references
({{var}}) and ${ENV} expansion are described in the
engine reference.
Application & context steps
Section titled “Application & context steps”install_application
Section titled “install_application”Install a WASM application on a node.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
path |
yes | string | Path to the .wasm file. |
dev |
no | bool (false) |
Install in dev mode. |
- type: install_application node: calimero-node-1 path: ./workflow-examples/res/kv_store.wasm dev: true outputs: app_id: applicationIduninstall_application
Section titled “uninstall_application”Remove an installed application. Fields: node, application_id.
- type: uninstall_application node: calimero-node-1 application_id: '{{app_id}}'get_application
Section titled “get_application”Read an application record. Fields: node, application_id.
- type: get_application node: calimero-node-1 application_id: '{{app_id}}' outputs: bytecode_blob: application.blob.bytecodelist_application_versions
Section titled “list_application_versions”List retained bytecode blob versions for an application. Fields: node,
application_id.
- type: list_application_versions node: calimero-node-1 application_id: '{{app_id}}'create_context
Section titled “create_context”Create a context bound to an application inside a namespace/group.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
application_id |
yes | string | Application to instantiate. |
group_id |
yes | string | Namespace/group the context belongs to. |
service_name |
no | string | Optional service name. |
- type: create_context node: calimero-node-1 application_id: '{{app_id}}' group_id: '{{namespace_id}}' outputs: context_id: contextId member_key: memberPublicKeydelete_context
Section titled “delete_context”Delete a context. Fields: node, context_id. The node resolves the acting identity from the authenticated session.
- type: delete_context node: calimero-node-1 context_id: '{{context_id}}'create_identity
Section titled “create_identity”Create a new identity (keypair) on a node.
- type: create_identity node: calimero-node-2 outputs: private_key: privateKey public_key: publicKeyjoin_context
Section titled “join_context”Join an existing context via group membership (the node must already be a group
member). Fields: node, context_id.
- type: join_context node: calimero-node-2 context_id: '{{context_id}}' outputs: member_identity: memberIdentityjoin_subgroup_inheritance
Section titled “join_subgroup_inheritance”Materialize a node’s inherited Open-subgroup membership without an admin-signed
invitation. Fields: node, group_id (the Open subgroup).
- type: join_subgroup_inheritance node: calimero-node-2 group_id: '{{subgroup_id}}'Namespace & group steps
Section titled “Namespace & group steps”A namespace is a root group. These steps manage namespaces, subgroups, and their membership.
create_namespace
Section titled “create_namespace”Create a namespace tied to an application. Deprecated alias: create_group.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
application_id |
yes | string | Application for the namespace. |
app_key |
no | string | Hex 32-byte bytecode blob id pinning a specific app version. |
- type: create_namespace node: calimero-node-1 application_id: '{{app_id}}' outputs: namespace_id: namespaceIdcreate_namespace_invitation
Section titled “create_namespace_invitation”Create an invitation to a namespace. Deprecated alias:
create_group_invitation.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
namespace_id |
yes* | string | Namespace to invite to (group_id accepted as deprecated alias). |
recursive |
no | bool (false) |
Create a recursive invitation. |
- type: create_namespace_invitation node: calimero-node-1 namespace_id: '{{namespace_id}}' outputs: invitation: invitationjoin_namespace
Section titled “join_namespace”Join a namespace using an invitation. Deprecated alias: join_group.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
namespace_id |
yes* | string | Namespace to join (group_id accepted as deprecated alias). |
invitation |
yes | string | Invitation data. |
- type: join_namespace node: calimero-node-2 namespace_id: '{{namespace_id}}' invitation: '{{invitation}}' outputs: member_identity: memberIdentity member_account: memberAccountmemberIdentity is the bs58 key the node signs with; memberAccount is the
64-hex account governance rows are keyed by. They are different values and are
not interchangeable — see Naming a member.
create_group_in_namespace
Section titled “create_group_in_namespace”Create a subgroup under a namespace.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
namespace_id |
yes | string | Parent namespace. |
group_name |
no | string | Subgroup display name. |
visibility |
no | open|restricted |
Birth visibility (default: server default restricted). |
- type: create_group_in_namespace node: calimero-node-1 namespace_id: '{{namespace_id}}' group_name: engineering visibility: openreparent_group
Section titled “reparent_group”Atomically move a subgroup to a new parent within the same namespace. Replaces
the removed nest_group/unnest_group steps.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
child_group_id |
yes | string | Group to move. |
new_parent_id |
yes | string | New parent group. |
- type: reparent_group node: calimero-node-1 child_group_id: '{{child_id}}' new_parent_id: '{{parent_id}}'Namespace & group queries
Section titled “Namespace & group queries”Read-only lookups. Each takes node plus the id shown.
| Step | Extra fields | Reads |
|---|---|---|
list_namespaces |
— | Namespaces on the node. |
node_identity |
— | Who this node is: accountId, deviceId, publicKey, accountRootPublicKey. Takes no namespace — none of it varies by one. |
list_namespace_groups |
namespace_id |
Subgroups of a namespace. |
list_subgroups |
group_id |
Subgroups of a group. |
get_group_info |
group_id |
Full group record. |
list_group_contexts |
group_id |
Contexts registered in a group. |
list_group_members |
group_id |
Members of a group. |
- type: list_group_members node: calimero-node-1 group_id: '{{namespace_id}}'Membership & capabilities steps
Section titled “Membership & capabilities steps”Naming a member
Section titled “Naming a member”A node has two ids in a namespace and they are not interchangeable.
Its signing key is bs58; its account is 64 hex characters, and that is the principal governance rows are keyed by.
The encodings differ on purpose, so passing one where the other belongs is a 400 rather than a silent no-op.
Read them from join_namespace (memberIdentity / memberAccount) or node_identity (publicKey / accountId).
Use the account everywhere: member_id on every member-addressing step, remove_group_members, and add_group_members’ identity.
That last one also accepts a key, for a subject the node holds no account for yet; it is resolved to an account on apply and refused if bound to none.
add_group_members
Section titled “add_group_members”Add members with roles to a group.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
group_id |
yes | string | Group to add to. |
members |
yes | list of {identity, role} |
Members and their roles. identity is an account, or a signing key for a subject with no account here yet. |
- type: add_group_members node: calimero-node-1 group_id: '{{group_id}}' members: - identity: '{{member_account}}' role: Memberremove_group_members
Section titled “remove_group_members”Remove members. Fields: node, group_id, members (list of accounts).
- type: remove_group_members node: calimero-node-1 group_id: '{{group_id}}' members: - '{{member_account}}'update_member_role
Section titled “update_member_role”Change a member’s role. Fields: node, group_id, member_id, role
(Admin, Member, ReadOnly — case-insensitive variants accepted).
- type: update_member_role node: calimero-node-1 group_id: '{{group_id}}' member_id: '{{member_account}}' role: Adminset_member_capabilities
Section titled “set_member_capabilities”Set a member’s capability bitmask. Fields: node, group_id, member_id,
capabilities (u32 integer). Read it back with get_member_capabilities.
- type: set_member_capabilities node: calimero-node-1 group_id: '{{group_id}}' member_id: '{{member_account}}' capabilities: 7set_default_capabilities
Section titled “set_default_capabilities”Set the group’s default capability bitmask for new members. Fields: node,
group_id, capabilities (u32).
- type: set_default_capabilities node: calimero-node-1 group_id: '{{group_id}}' capabilities: 3set_member_auto_follow
Section titled “set_member_auto_follow”Toggle a member’s auto-follow flags for new contexts and nested subgroups.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
group_id |
yes | string | Group. |
member_id |
yes | string | Target member account (64 hex). |
auto_follow_contexts |
yes | bool | Auto-join new contexts in this group. |
auto_follow_subgroups |
yes | bool | Self-admit into nested subgroups. |
- type: set_member_auto_follow node: calimero-node-1 group_id: '{{group_id}}' member_id: '{{member_account}}' auto_follow_contexts: true auto_follow_subgroups: falseset_subgroup_visibility
Section titled “set_subgroup_visibility”Set a subgroup’s visibility. open inherits parent members that hold
CAN_JOIN_OPEN_SUBGROUPS; restricted requires explicit membership.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
group_id |
yes | string | Subgroup. |
visibility |
yes | open|restricted |
New visibility (case-insensitive). |
- type: set_subgroup_visibility node: calimero-node-1 group_id: '{{subgroup_id}}' visibility: openLeave steps
Section titled “Leave steps”leave_context, leave_group, and leave_namespace remove the calling node
from a context, group, or namespace. Each takes node plus the relevant id.
- type: leave_namespace node: calimero-node-2 namespace_id: '{{namespace_id}}'Metadata & group settings steps
Section titled “Metadata & group settings steps”Metadata setters and getters
Section titled “Metadata setters and getters”Metadata records are string→string maps with an optional record name. The node resolves the acting identity from the authenticated session.
| Step | Fields |
|---|---|
set_group_metadata |
node, group_id, record_name?, data? |
get_group_metadata |
node, group_id |
set_member_metadata |
node, group_id, member_id, record_name?, data? |
get_member_metadata |
node, group_id, member_id |
set_context_metadata |
node, group_id, context_id, record_name?, data? |
get_context_metadata |
node, group_id, context_id |
- type: set_group_metadata node: calimero-node-1 group_id: '{{group_id}}' record_name: team-info data: owner: alice tier: golddetach_context_from_group
Section titled “detach_context_from_group”Detach a context from its group. Fields: node, group_id, context_id.
- type: detach_context_from_group node: calimero-node-1 group_id: '{{group_id}}' context_id: '{{context_id}}'sync_group
Section titled “sync_group”Diagnostic: trigger governance sync for a group. Fields: node, group_id.
- type: sync_group node: calimero-node-2 group_id: '{{group_id}}'register_group_signing_key
Section titled “register_group_signing_key”Register a signing key for a group. Fields: node, group_id, signing_key
(64 hex chars or a {{placeholder}}).
- type: register_group_signing_key node: calimero-node-1 group_id: '{{group_id}}' signing_key: '{{admin_signing_key}}'Delete steps
Section titled “Delete steps”delete_group and delete_namespace delete a group / namespace. Each takes
node and the id (group_id / namespace_id). The node resolves the acting identity from the authenticated session.
- type: delete_namespace node: calimero-node-1 namespace_id: '{{namespace_id}}'Deprecated invitation aliases
Section titled “Deprecated invitation aliases”These older, context-centric step names still validate but map to namespace invitation flows. Prefer the namespace steps above and the open-invitation steps below in new workflows.
| Step | Maps to | Fields |
|---|---|---|
invite, invite_identity |
namespace invitation | node, namespace_id (or group_id), recursive? |
join |
join namespace | node, namespace_id (or group_id), invitation |
invite_open |
open invitation (see below) | node, context_id, granter_id, valid_for_blocks? |
join_open |
join via open invitation | node, namespace_id (or group_id), invitation |
create_group |
create_namespace |
see create_namespace |
create_group_invitation |
create_namespace_invitation |
see above |
join_group |
join_namespace |
see above |
invite_open
Section titled “invite_open”Create an open invitation anyone can use to join a context.
- type: invite_open node: calimero-node-1 context_id: '{{context_id}}' granter_id: '{{admin_pub}}' valid_for_blocks: 2000 outputs: invitation: invitationjoin_open
Section titled “join_open”Join a context via an open invitation.
- type: join_open node: calimero-node-2 invitee_id: '{{new_member_pub}}' invitation: '{{invitation}}'Account identity steps
Section titled “Account identity steps”One person, many devices. An account is the principal a grant names; a device is a key that signs for it. So one account grant covers every device its holder enrols, and revoking a device withdraws that one key without touching the account.
An ordering constraint worth knowing before writing a scenario: a device link
travels as an encrypted group operation, so account_create must run after
the node has joined the context and holds the scope key. Placed before the join it
deadlocks.
A worked scenario across all of these lives in
workflow-examples/account-identity.yml.
account_create
Section titled “account_create”Enrol a device for a fresh account in a namespace, publishing the device link.
Exports the genesis halves a second device needs — pass them to account_pair.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Node that enrols the account. |
namespace_id |
yes | string | Namespace to enrol in. |
Outputs: accountId, deviceId, accountRootKey, accountNonce.
- type: account_create node: calimero-node-2 namespace_id: '{{namespace_id}}' outputs: account: accountId root_key: accountRootKey nonce: accountNonceaccount_pair
Section titled “account_pair”Pair a second node onto an account that already exists elsewhere, running both
halves of the exchange. One step rather than two because the ordering is forced:
the new device cannot mint its DeviceId until it knows the account (the id is
H(account ‖ nonce)), and the holder cannot certify that device until it knows the
id and both of its keys.
The step also compares the confirmation code both sides derive and fails on a mismatch — that comparison is the security promise of pairing, and forwarding the code unchecked would make this step the very “pasted alongside the keys it describes” channel the code exists to defeat.
The paired node needs no membership of its own: being a device of a member’s account is its whole route in.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | The new device’s node. |
holder |
yes | string | Node that already holds the account root. |
namespace_id |
yes | string | Namespace the account lives in. |
root_key |
yes | string | Genesis root key, from account_create. |
nonce |
yes | string | Genesis nonce, from account_create. |
Outputs: accountId, deviceId, keyDelivered.
- type: account_pair node: calimero-node-3 # the new device holder: calimero-node-2 # holds the account root namespace_id: '{{namespace_id}}' root_key: '{{root_key}}' nonce: '{{nonce}}' outputs: paired_device: deviceIdaccount_revoke
Section titled “account_revoke”Withdraw a device from its account and rotate the scope key, so the revoked device can neither write nor read past the rotation. Run on a node with the authority — an admin, or the account itself. A device cannot revoke itself.
keyRotated is exported so a scenario asserts the rotation happened rather than
inferring it from a later read that might pass for other reasons.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Admin, or the account itself. |
namespace_id |
yes | string | Namespace the device is bound in. |
device_id |
yes | string | Device to withdraw. |
Outputs: keyRotated.
- type: account_revoke node: calimero-node-1 namespace_id: '{{namespace_id}}' device_id: '{{paired_device}}' outputs: rotated: keyRotatedaccount_show
Section titled “account_show”Report which account a node speaks for in a namespace, and the device it holds there. A read: it mints nothing, and the account id exists whether or not a device was ever enrolled, because it is derived from the node’s root and the namespace.
deviceId comes back null when the node holds no device there. That is a real
answer rather than missing data, and it is what makes this step usable for
asserting a revocation stuck.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Node to ask. |
namespace_id |
yes | string | Namespace to report on. |
Outputs: accountId, deviceId.
# One account, two devices — the whole point of the account plane.- type: account_show node: calimero-node-2 namespace_id: '{{namespace_id}}' outputs: laptop: accountId- type: account_show node: calimero-node-3 namespace_id: '{{namespace_id}}' outputs: phone: accountId- type: json_assert statements: - 'json_equal({{laptop}}, {{phone}})'node_exec
Section titled “node_exec”Run an offline merod subcommand against a stopped node’s data directory, in
a one-shot container from the node’s own image.
Some operations are CLI-only and cannot run against a live node: merod account export|import opens the datastore directly and RocksDB’s lock is exclusive. That
rules out both obvious routes — docker exec needs a running container, and the
admin api does not expose the recovery key (serving a secret whose point is to live
offline over HTTP would be the wrong shape). What works is that the data directory
is a host bind mount and the image’s entrypoint is merod itself.
The image and mount are read off the existing container rather than reconstructed
from workflow config, so the step cannot disagree with how the node was actually
started. A running node is refused unless allow_running says otherwise —
attempting it yields an opaque lock error several layers down, which reads like a
node fault.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Node whose data directory to run against. |
args |
yes | list[string] | Subcommand and flags, e.g. ['account', 'export']. |
files |
no | map | Container path → contents, written into the mount before running. Paths outside /app/data are refused. |
allow_running |
no | bool | Run even if the node is up. Unsafe for anything that opens the datastore. |
expected_failure |
no | bool | Assert the command is refused rather than succeeding. |
Outputs: stdout, stdout_first_line (the value a single-value command emits,
ahead of its advisory output), stderr, exit_code.
- type: stop_node node: calimero-node-2
- type: node_exec node: calimero-node-2 args: ['account', 'export'] outputs: phrase: stdout_first_line
# `files:` is how a command that reads a file gets its input — no stdin plumbing.# Importing over an existing root is refused, and that refusal is the guard worth# asserting: a root that already certified devices has no second copy.- type: node_exec node: calimero-node-2 files: /app/data/recovery.txt: '{{phrase}}' args: ['account', 'import', '--from', '/app/data/recovery.txt'] expected_failure: true
- type: start_node node: calimero-node-2Delegated authorship steps
Section titled “Delegated authorship steps”A member that holds no node can still author its own writes: it signs a warrant naming one method, one operator and one nonce; a relay runs the method; the result is attributed to the member. This is the case a thin client is in — a device with only a signing key can neither run the application (the runtime is a JIT) nor decrypt the state (it never received a scope key).
Three things a scenario must do first, and none is implied by the others:
- Add the author’s ACCOUNT as a member of the group owning the context. Its device joins nothing — it is in no group’s binding rows and never will be, so a key-based membership check would refuse every delegated write.
- Grant the relay
CAN_AUTHOR_ON_BEHALFon that group. Not implied by membership, and not implied by admin. Without it the intent is refused before anything executes. - Mint a warrant with
sign_warrant, then spend it withperform_intent.
A warrant is single-use. Re-presenting a spent one is refused, and that is the point of the nonce ledger rather than a limitation: the signature stays valid forever, so replay is not forgery and the envelope check cannot be what stops it.
sign_warrant
Section titled “sign_warrant”Sign a member’s consent for one delegated write. Contacts nothing — no node
field, and that is deliberate rather than an omission: the key that signs a
warrant must never reach the node that runs the request, or that node could forge
writes in the member’s name.
The key material comes from the scenario. merobox cannot mint it and should not:
account_pair binds a device to a node and never hands the secret out. Supply
either a fixed test credential or one minted out-of-band with
merod account sign-cert.
Note the encodings, which are core’s and are not interchangeable: context_id is
base58, executor is hex. The author’s account is read out of credential
rather than configured, so it cannot disagree with the key that signs.
| Field | Required | Type | Meaning |
|---|---|---|---|
context_id |
yes | string | Context the intent runs in, base58. |
executor |
yes | string | Account allowed to spend it, hex — the relay’s, from node_identity. |
method |
yes | string | Method the warrant authorises. |
args |
no | mapping | Arguments it commits to. Placeholders resolve. |
device_secret |
yes | string | The author’s device signing secret, 64 hex chars. Never sent. |
credential |
yes | string | Hex device credential proving that key belongs to the author’s account. |
nonce |
no | int | Monotonic per author device. Default 1. |
valid_for |
no | int | Seconds the relay will still spend it for. Default 300. |
Outputs: warrant, authorAccount, authorDeviceKey, intentHash, notAfter.
Only H(method, args) is committed to, never the plaintext — the envelope this
rides in is readable by anything subscribed to the context’s topic, so the method
and its arguments stay sealed and only their hash travels in the clear.
- type: sign_warrant context_id: '{{context_id}}' executor: '{{identity_account_id_calimero-node-1}}' method: set args: key: delegated value: from-a-member-with-no-node device_secret: '{{author_secret}}' credential: '{{author_credential}}' nonce: 1 outputs: warrant: warrant author: authorAccountperform_intent
Section titled “perform_intent”Have a node run one method on a member’s behalf. The relay executes and signs the envelope with its own key; the change is attributed to the author, and both halves travel together so every peer re-checks that the member consented rather than taking the relay’s word.
Only the author’s half is sent. The node attaches its own credential, so a scenario never learns which of the node’s processes runs the intent, and a re-key on its side does not void a warrant already minted.
rootHash is exported because it is the node’s own claim that the run changed
something. Assert on it rather than on the status: an accepted intent that
advanced no state is a real failure mode, and it is far cheaper to catch here
than several steps later on a value that was never written.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | The relay: the node that runs the method. |
context_id |
yes | string | Context to run in. |
method |
yes | string | Method to run — must match the warrant. |
args |
no | mapping | Arguments — must match what the warrant committed to. |
warrant |
yes | string | Hex warrant, from a sign_warrant step. |
author_proof |
yes | string | The author’s hex device credential, the same one the warrant was signed against. |
Outputs: rootHash, returns.
- type: perform_intent node: calimero-node-1 context_id: '{{context_id}}' method: set args: key: delegated value: from-a-member-with-no-node warrant: '{{warrant}}' author_proof: '{{author_credential}}' outputs: root: rootHashBoth steps need a calimero-client-py carrying the sign_warrant and
perform_intent bindings, and a node whose core has the warrant types.
Call & auth steps
Section titled “Call & auth steps”Invoke a method on an application in a context (JSON-RPC).
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
context_id |
yes | string | Context to call in. |
method |
yes | string | Method name. |
args |
no | map | Method arguments (dynamic values resolved recursively). |
executor_public_key |
no | string | Identity to execute as. |
expected_failure |
no | bool (false) |
Pass only if the call is rejected. |
unauthenticated |
no | bool (false) |
Force a no-token request (negative auth test). |
- type: call node: calimero-node-1 context_id: '{{context_id}}' method: set args: key: hello value: world executor_public_key: '{{member_key}}' outputs: set_result: resultNegative test — assert an unauthenticated call is rejected with 401:
- type: call node: calimero-node-1 context_id: '{{context_id}}' method: get args: { key: hello } unauthenticated: true expected_failure: trueAuthenticate against a node’s embedded auth (auth_mode: embedded) and seed its
token cache so later call/ws_connect steps on that node are authenticated.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Node to authenticate against. |
username |
yes | string | Username (public key). |
password |
yes | string | Password. |
bootstrap_secret |
no | string | Out-of-band secret to mint the first root key (defaults from MERO_AUTH_BOOTSTRAP_SECRET). |
expected_failure |
no | bool (false) |
Pass only if authentication is rejected. |
- type: login node: calimero-node-1 username: alice password: password123 outputs: access_token: access_token refresh_token: refresh_tokenrefresh
Section titled “refresh”Refresh a node’s cached access token (POST /auth/refresh). Fields: node,
expected_failure?.
- type: refresh node: calimero-node-1 outputs: access_token: access_tokenws_connect
Section titled “ws_connect”Open a WebSocket subscription (/ws), attaching the cached JWT. Alias:
ws_subscribe.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Node to connect to. |
unauthenticated |
no | bool (false) |
Connect without a token (negative test). |
expected_failure |
no | bool (false) |
Pass only if the handshake is rejected. |
token |
no | string | Explicit JWT (overrides the cached token). |
message |
no | string | Text frame to send once connected. |
timeout |
no | float | Handshake timeout (seconds). |
- type: ws_connect # uses the token seeded by `login` node: calimero-node-1
- type: ws_connect node: calimero-node-1 unauthenticated: true expected_failure: true # no token -> handshake rejectedApplication upgrade & migration steps
Section titled “Application upgrade & migration steps”upgrade_group
Section titled “upgrade_group”Upgrade a group to a target application version.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
group_id |
yes | string | Group to upgrade. |
target_application_id |
yes | string | Application to upgrade to. |
cascade |
no | bool (false) |
Dispatch as a namespace cascade instead of a per-group upgrade. |
- type: upgrade_group node: calimero-node-1 group_id: '{{group_id}}' target_application_id: '{{new_app_id}}'cascade_namespace_application
Section titled “cascade_namespace_application”Cascade an application to every descendant of a namespace. Fields: node,
namespace_id, target_application_id.
- type: cascade_namespace_application node: calimero-node-1 namespace_id: '{{namespace_id}}' target_application_id: '{{new_app_id}}'assert_cascade_complete
Section titled “assert_cascade_complete”Poll until a namespace cascade finishes, or fail.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
namespace_id |
yes | string | Namespace whose cascade must complete. |
timeout_seconds |
no | number (30) |
Max seconds to poll. |
poll_interval |
no | number (2.0) |
Seconds between polls. |
- type: assert_cascade_complete node: calimero-node-1 namespace_id: '{{namespace_id}}' timeout_seconds: 60assert_migration_complete
Section titled “assert_migration_complete”Poll until a namespace migration finishes, or fail. Same fields as
assert_cascade_complete (node, namespace_id, timeout_seconds?,
poll_interval?).
- type: assert_migration_complete node: calimero-node-1 namespace_id: '{{namespace_id}}'Upgrade & migration queries
Section titled “Upgrade & migration queries”| Step | Fields | Reads / does |
|---|---|---|
get_group_upgrade_status |
node, group_id |
Per-group upgrade status. |
get_cascade_status |
node, namespace_id |
Cascade subtree status. |
get_migration_status |
node, namespace_id |
Migration rollup. |
abort_migration |
node, namespace_id |
Abort an in-flight migration. |
retry_group_upgrade |
node, group_id |
Retry a failed group upgrade. |
resync_context |
node, context_id, force? |
Resync a context from a peer (force: true discards local heads). |
- type: resync_context node: calimero-node-2 context_id: '{{context_id}}' force: trueget_migration_status and assert_migration_complete flatten the response into
target_version, expected_members, fleet_completed_at,
cohort_pinned_at_hlc, the rollup counters (total, migrated, in_progress,
unknown, failed, all_migrated, members_pending_signature), and one
members row per cohort member carrying its whole report (schema_version,
residue_auto, synced_up_to_hlc, reported_at, authored_remaining,
migration_failed).
Any of those the node did not send is left out of the summary rather than offered as null, so capturing it fails the step naming the field.
A node that has not converged omits fleet_completed_at, and a member that has not reported omits its whole report.
A field the node genuinely sends as null still binds, and the rollup counters are computed rather than passed through, so they are always present.
Alongside those, merobox recomputes these aggregates from the raw member reports rather than reading core’s rollup, so an assertion on them can falsify the rollup rather than confirm it against itself:
| Output | Meaning |
|---|---|
reported_at_target |
Members whose reported ABI state version is at or above target_version. |
reported_below_target |
Members that reported a version below the target. |
reported_missing |
Members that reported no usable state version at all. |
min_reported_schema_version |
Lowest reported state version, or None. |
residue_total |
Sum of residue_auto across members. |
failure_reasons |
Sorted list of the members’ migration_failed reasons. |
stuck_members |
Sorted peers behind those reasons. |
report.schema_version is a u32 ABI state version, unlike an application’s
own schema_info, which is a semver string compiled into the binary and reports
which WASM is loaded rather than whether the state was migrated. Assert
reported_below_target to check the latter.
The two target-relative counters are None when the response carries no
target_version (a namespace with no upgrade record): defaulting the target to
0 would classify every member as at-target and report green for a namespace
that never migrated.
Proposal steps
Section titled “Proposal steps”Read proposals in a context. Each takes node and context_id;
proposal-specific steps also take proposal_id.
| Step | Fields |
|---|---|
list_proposals |
node, context_id |
get_proposal |
node, context_id, proposal_id |
get_proposal_approvers |
node, context_id, proposal_id |
- type: get_proposal node: calimero-node-1 context_id: '{{context_id}}' proposal_id: '{{proposal_id}}'Blob steps
Section titled “Blob steps”upload_blob
Section titled “upload_blob”Upload a file to a node’s blob store. Fields: node, file_path, optional
context_id.
- type: upload_blob node: calimero-node-1 file_path: ./data/input.json context_id: '{{context_id}}' outputs: blob_id: blobIddelete_blob
Section titled “delete_blob”Delete a blob via the admin API (cascades chunked blobs).
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node. |
blob_id |
yes | string | Base58 (parent) blob id. |
missing_ok |
no | bool (true) |
Treat an already-absent blob as success. |
- type: delete_blob node: calimero-node-1 blob_id: '{{blob_id}}'delete_blob_on_disk
Section titled “delete_blob_on_disk”Delete a blob directly from a container’s on-disk blob store (for corruption / backfill tests).
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Target node (container name). |
blob_id |
yes | string | Base58 blob id. |
data_dir |
no | string (/app/data) |
CALIMERO_HOME in the container. |
blobs_subdir |
no | string (blobs) |
Blob-store subdir under <data_dir>/<node>. |
missing_ok |
no | bool (true) |
Treat a node that never held the blob as success. |
- type: delete_blob_on_disk node: calimero-node-1 blob_id: '{{blob_id}}'Control-flow steps
Section titled “Control-flow steps”Pause for a fixed duration. Fields: seconds (int ≥ 0), optional message.
- type: wait seconds: 5 message: Waiting for gossip to propagatewait_for_sync
Section titled “wait_for_sync”Poll until state converges across nodes. At least one of context_id /
group_id is required.
| Field | Required | Type | Meaning |
|---|---|---|---|
nodes |
yes | list | Nodes to wait for. |
context_id |
one of context_id/group_id |
string | Wait for contextStateHash to converge. |
group_id |
one of context_id/group_id |
string | Wait for groupStateHash to converge. |
timeout |
no | int (60) |
Timeout in seconds. |
check_interval |
no | float (2) |
Steady-state polling cap (backoff ceiling). |
initial_check_interval |
no | float | Starting sleep for adaptive backoff (default ~0.05s). |
backoff_factor |
no | float | Geometric growth per missed check (default 2.0). |
trigger_sync |
no | bool (false) |
Trigger a sync before waiting. |
- type: wait_for_sync context_id: '{{context_id}}' nodes: - calimero-node-1 - calimero-node-2 timeout: 60 trigger_sync: truerepeat
Section titled “repeat”Run nested steps N times. Iteration counters (iteration, iteration_index,
total_iterations, …) are exported automatically for use in nested steps and
can be renamed via outputs.
| Field | Required | Type | Meaning |
|---|---|---|---|
count |
yes | int ≥ 1 | Number of iterations. |
steps |
yes | list | Steps to repeat. |
- type: repeat count: 3 outputs: current_iteration: iteration # rename the auto-exported counter steps: - type: call node: calimero-node-1 context_id: '{{context_id}}' method: set args: key: 'key_{{current_iteration}}' value: 'value_{{current_iteration}}'parallel
Section titled “parallel”Run several groups of steps concurrently. Steps within a group still run in order; the groups run in parallel.
| Field | Required | Type | Meaning |
|---|---|---|---|
groups |
yes | list of {name?, steps} |
Groups to run concurrently. |
failure_mode |
no | fail-slow|fail-fast|continue-on-error (fail-slow) |
How group failures are handled. |
mode |
no | burst|sustained|mixed (burst) |
Group start scheduling. |
- type: parallel failure_mode: fail-fast groups: - name: writer steps: - type: call node: calimero-node-1 context_id: '{{context_id}}' method: set args: { key: a, value: '1' } - name: reader steps: - type: call node: calimero-node-2 context_id: '{{context_id}}' method: get args: { key: a }script
Section titled “script”Run a shell script.
| Field | Required | Type | Meaning |
|---|---|---|---|
script |
yes | string | Path to the script. |
target |
no | image|nodes|local (image) |
Where to run: inside the image build, on running nodes, or on the host. |
description |
no | string | Human label. |
- type: script description: Assert container state script: ./workflow-examples/scripts/assert-container-state.sh target: localNode & network fault-injection steps
Section titled “Node & network fault-injection steps”stop_node / start_node
Section titled “stop_node / start_node”Stop or (re)start local nodes mid-workflow. nodes accepts a single name or a
list. start_node uses the node’s config from the workflow’s nodes: section.
| Step | Fields |
|---|---|
stop_node |
nodes |
start_node |
nodes, wait_for_ready? (bool, true), wait_timeout? (int, 30) |
- type: stop_node nodes: calimero-node-1
- type: start_node nodes: - calimero-node-1 wait_for_ready: true wait_timeout: 30pause_container / unpause_container / restart_container
Section titled “pause_container / unpause_container / restart_container”Docker-level container control. Each takes container. restart_container also
accepts wait_healthy (bool, true) and timeout (int).
- type: pause_container container: calimero-node-1
- type: restart_container container: calimero-node-1 wait_healthy: truedisconnect_node / connect_node
Section titled “disconnect_node / connect_node”Disconnect a node from (or reconnect it to) a Docker network. Fields: node,
optional network (defaults to the container’s attached network).
- type: disconnect_node node: calimero-node-1
- type: connect_node node: calimero-node-1partition_peers / heal_peers
Section titled “partition_peers / heal_peers”Cut (or restore) libp2p traffic between a node and specific peers while keeping RPC reachable. Linux + iptables + passwordless sudo only.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Container to isolate / reconnect. |
peers |
yes | list (≥ 1) | Peer containers to cut / restore. |
- type: partition_peers node: calimero-node-1 peers: - calimero-node-2
- type: heal_peers node: calimero-node-1 peers: - calimero-node-2inject_network_fault
Section titled “inject_network_fault”Apply a tc-based network fault to a container for a duration.
| Field | Required | Type | Meaning |
|---|---|---|---|
container |
yes | string | Target container. |
fault |
yes | loss|delay |
Fault type. |
duration |
yes | int ≥ 1 | How long to hold the fault (seconds). |
percent |
required for loss |
float (0, 100] | Packet-loss percent. |
ms |
required for delay |
int ≥ 1 | Added delay in ms. |
interface |
no | string (eth0) |
Container network interface. |
- type: inject_network_fault container: calimero-node-1 fault: loss percent: 20 duration: 10Assertion steps
Section titled “Assertion steps”assert
Section titled “assert”Evaluate boolean statements. Helpers include is_set(...), contains(a, b),
and equality (==). Statements may be strings or {statement, message}
mappings.
| Field | Required | Type | Meaning |
|---|---|---|---|
statements |
yes | list | Assertion statements. |
non_blocking |
no | bool | Continue the workflow on failure instead of aborting. |
- type: assert statements: - 'is_set({{set_result}})' - "contains({{get_result}}, 'world')"json_assert
Section titled “json_assert”Assert on JSON structure with json_equal(...) and json_subset(...). Field:
statements.
- type: json_assert statements: - 'json_subset({{user_data}}, {"email": "user@example.com"})'assert_log_present / assert_log_absent
Section titled “assert_log_present / assert_log_absent”Scan node logs for patterns. assert_log_present requires each pattern to hit
at least min_matches times; assert_log_absent fails if any pattern appears.
| Field | Required | Type | Meaning |
|---|---|---|---|
nodes |
yes | list | Nodes to scan ([] = all running nodes). |
patterns |
yes | list | Patterns to match. |
regex |
no | bool (false) |
Treat patterns as Python regex. |
tail_lines |
no | int | Only scan the last N lines per node. |
case_sensitive |
no | bool (true) |
Case-sensitive matching. |
min_matches |
present only | int (1) |
Required hits per pattern (assert_log_present only). |
- type: assert_log_present nodes: [calimero-node-1] patterns: - 'context synced'
- type: assert_log_absent nodes: [] # all running nodes patterns: - 'panicked'assert_api_response
Section titled “assert_api_response”GET a path on a node’s admin API and assert against the JSON the node actually
sent. Every other step reads its response through calimero-client-py, whose
compiled DTOs silently drop any key the pinned core build does not know about,
so a field added to core is unassertable until a client release lands. This step
never hands the body to a typed deserializer.
| Field | Required | Type | Meaning |
|---|---|---|---|
node |
yes | string | Node whose admin API is queried. |
path |
yes | string | Admin-API path, e.g. /admin-api/health. |
match |
one of | map | Dotted paths mapped to expected values. |
present |
one of | list | Dotted paths that must exist, whatever their value. |
absent |
one of | list | Dotted paths that must not exist. |
token |
no | string | Explicit JWT (otherwise the token login cached for the node). |
At least one of match / present / absent is required, so a step can never
silently assert nothing. Paths address the body verbatim: the admin API
serializes its payload directly, camelCase, with no envelope, so a field reads
fleetCompletedAt.
- type: assert_api_response node: calimero-node-1 path: '/admin-api/groups/{{namespace_id}}/migration-status' match: rollup.allMigrated: true present: - fleetCompletedAtCore marks optional fields skip_serializing_if = "Option::is_none", so a key
being absent is meaningful and distinct from present-and-null. The three
assertions separate the two states: present accepts a null value, absent
rejects one, and match: {path: null} demands the key be there and null - an
absent key fails it and is reported as absent, not as null.
# A namespace whose fleet never converged must not emit the stamp at all.- type: assert_api_response node: calimero-node-1 path: '/admin-api/groups/{{namespace_id}}/migration-status' absent: - fleetCompletedAtA failure prints the per-path verdict and then the whole response body, so CI
output alone tells “the field is missing” from “the field is wrong”. The step
issues one request and does not poll; wrap it in repeat or precede it with
wait_for_sync when the assertion has to wait for state to settle. A non-2xx
status fails the step and works with expected_failure / expected_error:
- type: assert_api_response node: calimero-node-1 path: '/admin-api/namespaces/deadbeef/migration-status' expected_failure: true expected_error: 'HTTP 404'Both govern the request outcome only, never the assertion verdicts. An
assertion that misses always fails the step: letting a miss satisfy
expected_failure would turn a typo in a path into a green negative test.
Mesh & fuzzy steps
Section titled “Mesh & fuzzy steps”create_mesh
Section titled “create_mesh”Create a context and wire multiple nodes into it via group membership in one step.
| Field | Required | Type | Meaning |
|---|---|---|---|
context_node |
yes | string | Node to create the context on. |
application_id |
yes | string | Application to instantiate. |
nodes |
yes | list | Nodes to join into the mesh (must include one ≠ context_node). |
params |
no | string | Init params as a JSON string. |
outputs: captures from the context response, plus namespaceId folded in:
| Source | Meaning |
|---|---|
contextId |
The context the mesh was built around. |
memberPublicKey |
The context node’s member key. |
namespaceId |
The namespace the context lives under - needed by create_group_in_namespace. |
The per-node ids the joins produce are set directly, since outputs: sees one
response and there is one join per node:
| Variable | Meaning |
|---|---|
{{namespace_id}} |
Same namespace id, for workflows not using outputs:. |
{{member_account_<node>}} |
Account the node’s key speaks for - what list_group_members returns and what every member-addressing step takes. |
{{member_identity_<node>}} |
The node’s member public key. |
- type: create_mesh context_node: calimero-node-1 application_id: '{{app_id}}' nodes: - calimero-node-2 - calimero-node-3 outputs: context_id: contextId member_public_key: memberPublicKey namespace_id: namespaceId
- type: create_group_in_namespace node: calimero-node-1 namespace_id: '{{namespace_id}}' group_name: private-channel outputs: subgroup_id: groupId
- type: update_member_role node: calimero-node-1 group_id: '{{subgroup_id}}' member_id: '{{member_account_calimero-node-2}}' role: Memberfuzzy_test
Section titled “fuzzy_test”Run long randomized load tests with weighted operation patterns. See fuzzy load testing for the full model.
| Field | Required | Type | Meaning |
|---|---|---|---|
duration_minutes |
yes | number > 0 | How long to run. |
context_id |
yes | string | Context to hammer. |
nodes |
yes | list of {name, executor_key} |
Nodes and their executor keys. |
operations |
yes | list of {name, weight, steps} |
Weighted operation patterns. |
Random generators ({{random_int(min, max)}}, {{uuid}}, {{timestamp}},
{{random_node}}, …) and auto-captured args ({{fuzzy_key}}, {{fuzzy_value}})
are available inside operation steps.
- type: fuzzy_test duration_minutes: 30 context_id: '{{context_id}}' nodes: - name: calimero-node-1 executor_key: '{{key1}}' operations: - name: write_and_read weight: 70 steps: - type: call node: '{{random_node}}' method: set context_id: '{{context_id}}' args: key: 'k_{{random_int(1, 1000)}}' value: 'v_{{uuid}}' - type: assert non_blocking: true statements: - 'is_set({{result}})'TEE steps
Section titled “TEE steps”These drive a local mock-TEE fleet against a node’s admin API. They require a
merod built with the mock-attestation feature and nodes booted with
mock_tee: true.
set_tee_admission_policy
Section titled “set_tee_admission_policy”Set a namespace-root TEE admission policy. Fields: node, group_id, optional
accept_mock (bool, true).
- type: set_tee_admission_policy node: tee-owner group_id: '{{namespace_id}}' accept_mock: truetee_fleet_join
Section titled “tee_fleet_join”Run a mock-TEE fleet join against a namespace root. Fields: node, group_id.
Blocks server-side for one admission window.
- type: tee_fleet_join node: tee-replica group_id: '{{namespace_id}}'assert_tee_member / assert_not_member
Section titled “assert_tee_member / assert_not_member”Assert presence / absence of an account in a group’s member list. Fields:
node, group_id, account.
Takes the account, not the signing key: membership is recorded against the
account a key speaks for, so the listing reports accounts and a key matches
nothing. tee_fleet_join reports both — capture account.
- type: assert_tee_member node: tee-owner group_id: '{{namespace_id}}' account: '{{replica_account}}'See also
Section titled “See also”- Workflow engine — execution model, variables, retries.
- Testing with merobox — E2E scenarios and fuzzy load testing.
- CLI reference —
merobox bootstrapcommands. - System overview and glossary.