Skip to content

commands

commands

The signed command envelope, and the eleven checks it must survive.

A command crossing between devices is the mesh's sharpest edge: it makes one machine act on another's word. Spec §9 lists what must be verified, and the value of the list is that it is verified in ONE place — a check spread across call sites is a check that will be forgotten at one of them.

identity · origin · destination · signature · expiry · nonce ·
idempotency · permissions · capabilities · risk · confirmation

Two properties are worth stating plainly, because they are what make the difference between a protocol and a hope:

  • The signature covers the whole envelope EXCEPT itself, over canonical bytes. Change one character of one argument and verification fails.
  • A nonce may be spent once. Replaying a captured command — the classic way to make "open this" become "open this forty times" — is refused by the second attempt, even with a perfect signature.

Classes

CommandError

Bases: RuntimeError

Malformed command — the sender got the protocol wrong.

CommandRejected

CommandRejected(code: str, message: str)

Bases: RuntimeError

A well-formed command that must not run.

Carries a machine-readable code for the caller's status field and a French message safe to show the user (never the failing signature, never a nonce, never an argument value).

Source code in src/diapason/mesh/commands.py
def __init__(self, code: str, message: str) -> None:
    super().__init__(message)
    self.code = code
    self.message = message

RemoteCommand dataclass

RemoteCommand(
    command_id: str,
    owner_id: str,
    origin_device_id: str,
    target_device_id: str,
    tool: str,
    arguments: dict[str, Any],
    created_at_ms: int,
    expires_at_ms: int,
    nonce: str,
    idempotency_key: str,
    requires_confirmation: bool = False,
    confirmation_id: str = "",
    signature: str = "",
    version: int = COMMAND_VERSION,
    _extra: dict[str, Any] = dict(),
)

One instruction from one device to another (spec §8).

Attributes
est_scelle property
est_scelle: bool

Le contenu de cette commande voyage-t-il chiffré ?

Methods:
to_dict
to_dict(*, with_signature: bool = True) -> dict[str, Any]

L'enveloppe telle qu'elle part sur le fil.

INVARIANT DU SCELLEMENT, et tout en dépend : .tool, .arguments et .requires_confirmation portent TOUJOURS le clair, des deux côtés du réseau. _extra["scelle"] porte le triplet chiffré exactement tel qu'il a été signé, et c'est LUI que cette méthode réémet.

Sans cette règle, le récepteur rangerait dans sa file une enveloppe portant le clair sous une signature calculée sur le chiffré — donc une enveloppe qui ne vérifierait plus sa propre signature. C'est le bogue que la première version du plan s'annonçait comme bénéfice avant de l'introduire.

Une commande CLAIRE (_extra vide) produit exactement les mêmes octets qu'avant le 26 août 2026. C'est ce qui protège le client mobile figé, et un test le compare à un vecteur gelé plutôt que de s'en remettre à la lecture.

Source code in src/diapason/mesh/commands.py
def to_dict(self, *, with_signature: bool = True) -> dict[str, Any]:
    """L'enveloppe telle qu'elle part sur le fil.

    INVARIANT DU SCELLEMENT, et tout en dépend : ``.tool``,
    ``.arguments`` et ``.requires_confirmation`` portent TOUJOURS le
    clair, des deux côtés du réseau. ``_extra["scelle"]`` porte le
    triplet chiffré exactement tel qu'il a été signé, et c'est LUI que
    cette méthode réémet.

    Sans cette règle, le récepteur rangerait dans sa file une enveloppe
    portant le clair sous une signature calculée sur le chiffré — donc
    une enveloppe qui ne vérifierait plus sa propre signature. C'est le
    bogue que la première version du plan s'annonçait comme bénéfice
    avant de l'introduire.

    Une commande CLAIRE (``_extra`` vide) produit exactement les mêmes
    octets qu'avant le 26 août 2026. C'est ce qui protège le client
    mobile figé, et un test le compare à un vecteur gelé plutôt que de
    s'en remettre à la lecture.
    """
    scelle = self._extra.get("scelle") or {}
    payload: dict[str, Any] = {
        "version": self.version,
        "commandId": self.command_id,
        "ownerId": self.owner_id,
        "originDeviceId": self.origin_device_id,
        "targetDeviceId": self.target_device_id,
        "tool": scelle.get("tool", self.tool) if scelle else self.tool,
        "arguments": (
            scelle.get("arguments", self.arguments) if scelle else self.arguments
        ),
        "createdAtMs": self.created_at_ms,
        "expiresAtMs": self.expires_at_ms,
        "nonce": self.nonce,
        "idempotencyKey": self.idempotency_key,
        "requiresConfirmation": (
            bool(scelle.get("requiresConfirmation"))
            if scelle
            else self.requires_confirmation
        ),
    }
    if self.confirmation_id:
        payload["confirmationId"] = self.confirmation_id
    if with_signature and self.signature:
        payload["signature"] = self.signature
    return payload

NonceStore

NonceStore(db_path: str | Path | None = None)

Spent nonces, so a captured command cannot be replayed.

Entries are pruned past the maximum command lifetime: a nonce can only be replayed while its command could still be valid, so remembering it beyond that adds storage without adding safety.

Source code in src/diapason/mesh/commands.py
def __init__(self, db_path: str | Path | None = None) -> None:
    self.db_path = Path(db_path or (get_data_dir() / "mesh.db"))
    self.db_path.parent.mkdir(parents=True, exist_ok=True)
    # `closing` ferme le descripteur, ce que le gestionnaire de contexte de
    # sqlite3 ne fait PAS : seul, il ne gère que la transaction, et chaque
    # appel fuyait donc un fichier ouvert jusqu'à « Too many open files ».
    # L'ordre est celui-ci et pas l'inverse : dans « with A, B », B est
    # quitté en premier, donc `conn` commit AVANT que `closing` ne ferme.
    with closing(self._connect()) as conn, conn:
        conn.execute(
            """CREATE TABLE IF NOT EXISTS mesh_nonces (
                   nonce TEXT PRIMARY KEY,
                   device_id TEXT NOT NULL,
                   seen_at_ms INTEGER NOT NULL
               )"""
        )
        conn.commit()
Methods:
spend
spend(
    nonce: str,
    device_id: str,
    *,
    retention_ms: int = 3600000,
) -> bool

Consume nonce. False when it was already spent — a replay.

The INSERT itself is the check: relying on the primary key makes the test atomic, where a SELECT-then-INSERT would let two concurrent deliveries of the same command both pass.

Source code in src/diapason/mesh/commands.py
def spend(
    self, nonce: str, device_id: str, *, retention_ms: int = 3_600_000
) -> bool:
    """Consume *nonce*. False when it was already spent — a replay.

    The INSERT itself is the check: relying on the primary key makes the
    test atomic, where a SELECT-then-INSERT would let two concurrent
    deliveries of the same command both pass.
    """
    stamp = now_ms()
    # Même paire qu'à la création : `conn` commit, puis `closing` ferme.
    # C'est le site le plus chaud du module — une commande reçue par
    # seconde suffisait à épuiser les descripteurs du processus.
    with closing(self._connect()) as conn, conn:
        conn.execute(
            "DELETE FROM mesh_nonces WHERE seen_at_ms < ?", (stamp - retention_ms,)
        )
        try:
            conn.execute(
                "INSERT INTO mesh_nonces(nonce, device_id, seen_at_ms) "
                "VALUES (?,?,?)",
                (nonce, device_id, stamp),
            )
        except sqlite3.IntegrityError:
            return False
        conn.commit()
    return True

Functions:

build_command

build_command(
    *,
    owner_id: str,
    origin_device_id: str,
    target_device_id: str,
    tool: str,
    arguments: Mapping[str, Any] | None = None,
    ttl_ms: int = DEFAULT_TTL_MS,
    requires_confirmation: bool = False,
    confirmation_id: str = "",
    idempotency_key: str = "",
) -> RemoteCommand

Assemble an unsigned command with fresh anti-replay material.

Source code in src/diapason/mesh/commands.py
def build_command(
    *,
    owner_id: str,
    origin_device_id: str,
    target_device_id: str,
    tool: str,
    arguments: Mapping[str, Any] | None = None,
    ttl_ms: int = DEFAULT_TTL_MS,
    requires_confirmation: bool = False,
    confirmation_id: str = "",
    idempotency_key: str = "",
) -> RemoteCommand:
    """Assemble an unsigned command with fresh anti-replay material."""
    created = now_ms()
    return RemoteCommand(
        command_id=f"cmd_{uuid.uuid4().hex}",
        owner_id=owner_id,
        origin_device_id=origin_device_id,
        target_device_id=target_device_id,
        tool=tool,
        arguments=dict(arguments or {}),
        created_at_ms=created,
        expires_at_ms=created + max(1_000, int(ttl_ms)),
        nonce=secrets.token_urlsafe(24),
        # Default idempotency is per-command, so a retry of the SAME envelope
        # is idempotent while two deliberate invocations are not conflated.
        idempotency_key=idempotency_key or f"idem_{uuid.uuid4().hex}",
        requires_confirmation=requires_confirmation,
        confirmation_id=confirmation_id,
    )

sign_command

sign_command(command: RemoteCommand) -> RemoteCommand

Sign with THIS device's private key, over everything but the signature.

Source code in src/diapason/mesh/commands.py
def sign_command(command: RemoteCommand) -> RemoteCommand:
    """Sign with THIS device's private key, over everything but the signature."""
    from diapason.mesh.identity import sign_envelope

    signature = sign_envelope(command.to_dict(with_signature=False))
    return RemoteCommand(**{**command.__dict__, "signature": signature})

verify_command

verify_command(
    raw: Mapping[str, Any],
    *,
    registry: Any,
    local_device_id: str,
    local_owner_id: str,
    nonces: NonceStore,
    now: int | None = None,
) -> RemoteCommand

Run every check of spec §9. Raises on the first failure.

Order matters: the cheap structural checks come first so a malformed or misaddressed command never reaches the cryptography, and the nonce is spent LAST — otherwise a command rejected for another reason would burn a nonce the legitimate sender still needs.

Source code in src/diapason/mesh/commands.py
def verify_command(
    raw: Mapping[str, Any],
    *,
    registry: Any,
    local_device_id: str,
    local_owner_id: str,
    nonces: NonceStore,
    now: int | None = None,
) -> RemoteCommand:
    """Run every check of spec §9. Raises on the first failure.

    Order matters: the cheap structural checks come first so a malformed or
    misaddressed command never reaches the cryptography, and the nonce is
    spent LAST — otherwise a command rejected for another reason would burn
    a nonce the legitimate sender still needs.
    """
    stamp = now_ms() if now is None else now
    command = RemoteCommand.from_dict(raw)

    # 1. protocol version — refuse what we cannot fully understand
    if command.version != COMMAND_VERSION:
        raise CommandRejected(
            "UNSUPPORTED", "Cette commande utilise une version non prise en charge."
        )

    # 2. identity — same fleet
    if not command.owner_id or command.owner_id != local_owner_id:
        raise CommandRejected(
            "DENIED", "Cette commande vient d'un autre ensemble d'appareils."
        )

    # 3. destination — addressed to us
    if command.target_device_id != local_device_id:
        raise CommandRejected(
            "DENIED", "Cette commande ne s'adresse pas à cet appareil."
        )

    # 4. origin — a device we know, trust, and hold a key for. A revoked
    #    device has no key here (registry returns None), so it stops at once.
    if command.origin_device_id == local_device_id:
        raise CommandRejected("DENIED", "Une commande ne peut pas venir d'elle-même.")
    public_key = registry.public_key_of(command.origin_device_id)
    if public_key is None:
        raise CommandRejected(
            "DENIED", "L'appareil émetteur n'est pas autorisé sur cet appareil."
        )

    # 5. expiry, with bounded clock tolerance in both directions
    if command.expires_at_ms <= command.created_at_ms:
        raise CommandRejected("EXPIRED", "Cette commande a une validité invalide.")
    if command.created_at_ms - MAX_CLOCK_SKEW_MS > stamp:
        raise CommandRejected("DENIED", "Cette commande est datée du futur.")
    if command.expires_at_ms + MAX_CLOCK_SKEW_MS < stamp:
        raise CommandRejected("EXPIRED", "Cette commande a expiré.")

    # 6. signature — over the envelope without itself
    from diapason.mesh.identity import verify_envelope

    if not command.signature:
        raise CommandRejected("DENIED", "Cette commande n'est pas signée.")
    if not verify_envelope(
        command.to_dict(with_signature=False), command.signature, public_key
    ):
        raise CommandRejected("DENIED", "La signature de cette commande est invalide.")

    # 6 bis. OUVRIR LE SCEAU, si l'enveloppe en porte un.
    #
    #        L'ordre est obligatoire, pas esthétique. APRÈS la signature :
    #        un inconnu du réseau ne doit pas pouvoir nous faire calculer un
    #        X25519 et un AES-GCM par paquet. AVANT le contrôle 7 : la
    #        sentinelle n'existe pas dans le catalogue, donc « l'outil
    #        n'existe pas » serait le seul message qu'on verrait jamais.
    #
    #        ET AVANT LE CONTRÔLE 11 : le nonce est dépensé en dernier. Un
    #        descellement raté ne doit pas le brûler, sinon l'émetteur
    #        légitime qui réessaie se ferait refuser pour rejeu — une panne
    #        dont la cause serait introuvable.
    from diapason.mesh.scellement import SENTINELLE, desceller_commande

    if command.tool == SENTINELLE:
        try:
            command = desceller_commande(command)
        except Exception as exc:  # noqa: BLE001
            raise CommandRejected(
                "DENIED", "Cette commande scellée n'a pas pu être ouverte."
            ) from exc

    # 7. tool must exist, be narrow, and be allowed remotely (spec §21)
    from diapason.mesh.tools import get_remote_tool

    spec = get_remote_tool(command.tool)
    if spec is None:
        raise CommandRejected(
            "UNSUPPORTED", f"L'outil « {command.tool} » n'existe pas."
        )

    # 8. arguments must match the tool's declared shape — no free-form passthrough
    spec.validate(command.arguments)

    # 9. capabilities — what THIS device can actually honour.
    #
    # Read from the platform ceiling, NOT from a registry row: a device is
    # never listed in its own registry, so looking itself up returned None
    # and this check quietly did nothing on every receiver. The sender's
    # identical check is not a substitute — it consults what the sender
    # recorded about us, which is exactly the thing an attacker would have
    # tampered with.
    from diapason.mesh.capabilities import local_capabilities

    if spec.capability not in local_capabilities():
        raise CommandRejected(
            "UNSUPPORTED",
            f"Cet appareil ne peut pas exécuter « {command.tool} ».",
        )

    # 10. risk / confirmation — an impactful tool may not run unconfirmed
    if spec.requires_confirmation and not command.requires_confirmation:
        raise CommandRejected(
            "DENIED",
            f"L'outil « {command.tool} » exige une confirmation explicite.",
        )

    # 11. nonce — spent last, and only once
    if not nonces.spend(command.nonce, command.origin_device_id):
        raise CommandRejected("DENIED", "Cette commande a déjà été reçue.")

    return command