langchain-ai/deepagents · error · ValueError

`snapshot_signing_key` must be a non-empty str or bytes.

Error message

`snapshot_signing_key` must be a non-empty str or bytes.

What it means

`normalize_signing_key` validates the HMAC key used to sign snapshot files. Keys must be a non-empty str or bytes: strs are UTF-8 encoded, bytes are used verbatim, and empty material is rejected because an empty HMAC key provides no integrity guarantee.

Source

Thrown at libs/partners/quickjs/langchain_quickjs/_snapshot.py:60

SnapshotRecord = tuple[str, bytes]

# Domain-separation prefix folded into every signed message so a snapshot HMAC
# can never be confused with an HMAC computed over some other blob using the
# same key. Bump the version suffix if the signed-message layout ever changes.
_HMAC_DOMAIN = b"langchain-quickjs/snapshot-hmac/v1"


def normalize_signing_key(key: str | bytes) -> bytes:
    """Coerce a user-supplied signing key into raw ``bytes``.

    ``str`` keys are UTF-8 encoded; ``bytes`` are used verbatim. Empty keys are
    rejected because an empty HMAC key provides no integrity guarantee.
    """
    material = key.encode("utf-8") if isinstance(key, str) else bytes(key)
    if not material:
        msg = "`snapshot_signing_key` must be a non-empty str or bytes."
        raise ValueError(msg)
    return material


def sign_snapshot(key: bytes, payload: bytes, thread_id: str) -> bytes:
    """Return the HMAC-SHA256 tag over a fully materialized snapshot.

    The tag is computed over the *completed materialized* snapshot bytes (the
    full heap serialization) bound to ``thread_id``, so a valid snapshot for one
    thread cannot be replayed into another by a state-store adversary. This is
    signed before the payload is delta-encoded (``encode_snapshot``) and flushed
    onto the ``bsdiff`` patch chain; verification recomputes the tag over the
    materialized bytes the chain replays back to.
    """
    return hmac.new(key, _signed_message(payload, thread_id), sha256).digest()


def verify_snapshot(
    key: bytes, payload: bytes, thread_id: str, tag: bytes | None

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Provide a non-empty secret string, e.g. `snapshot_signing_key=os.environ['SNAPSHOT_KEY']` after confirming it is set.
  2. Fail fast at startup: assert the key env var is present and non-empty before building the middleware.
  3. If no signing is intended, omit `snapshot_signing_key` (default None) instead of passing an empty value.

Example fix

// before
middleware = QuickJsMiddleware(snapshot_signing_key=os.environ.get("SNAPSHOT_KEY", ""))

// after
key = os.environ["SNAPSHOT_KEY"]
assert key, "SNAPSHOT_KEY must be set"
middleware = QuickJsMiddleware(snapshot_signing_key=key)
Defensive patterns

Strategy: validation

Validate before calling

key = os.environ.get("SNAPSHOT_KEY")
if not key:
    raise ValueError("SNAPSHOT_KEY must be a non-empty str or bytes")

Type guard

def is_valid_signing_key(key) -> bool:
    if isinstance(key, str):
        return bool(key.encode("utf-8"))
    return isinstance(key, (bytes, bytearray)) and bool(bytes(key))

Try / catch

try:
    mw = QuickJsMiddleware(snapshot_signing_key=key)
except ValueError as e:
    if "snapshot_signing_key" in str(e):
        fix_key_configuration()  # load from secret manager / abort startup

Prevention

When it happens

Trigger: Passing `snapshot_signing_key=""` or `b""` (or a falsy bytes-like object, e.g. `bytearray()`) when constructing the middleware or in direct calls to `normalize_signing_key`.

Common situations: Reading the signing key from an env var or config file that is empty/unset and passing the empty string through without checking; a template or secret manager returning an empty value; typo'd env var name yielding `''`.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/f984048f9a47deda. Report an issue: GitHub.