Sealing

What sealing a run means, when it happens, and when it does not.

Sealing is the act, performed once at the end of a run, of ordering the artifacts into a hash chain, building the manifest, and signing — with the organization's active signing key — a canonical signature block that carries the manifest's hash. Step 4 below sets out what that block holds, and why it, rather than the manifest, is the signed unit.

When it happens

The scheduler seals a run when the agent's container exits zero and returns a results payload, after those results are persisted and the engagement's status becomes completed. Sealing is the last thing that happens to a run.

That condition is weaker than "the agent finished the work". The agent returns its payload and exits zero from three endings:

  • it worked through the reporting phase and ran out of phases;
  • it reached the 500-step ceiling on the run as a whole;
  • it aborted after five consecutive failing steps.

All three are recorded as completed and all three are sealed. A run cut short at a ceiling is not distinguishable by its status from one that finished, and the seal over it is as valid as any other. The manifest's entry count and the report are what tell them apart.

Two outcomes do not lead to a seal. A container that exits non-zero marks the engagement failed. A container that exits zero but produces no results payload is also marked failed, rather than recorded as complete with nothing to show. When a run fails covers those paths.

A scope overlap produces a sealed locker with nothing in it

An in-scope entry that overlaps an out-of-scope entry is caught inside the container, while the rules-of-engagement document is being rendered in the first phase. The render raises before the phase advances, so the next step retries the same render and fails the same way. Five of those in a row abort the run, with no command executed and no artifact captured.

That run exits zero, is recorded as completed, and is sealed. Its chain has no entries, so its chain head is the run's genesis value and its manifest reports entry_count: 0. The seal and the signature are valid; they cover nothing. Read entry_count before treating a locker as evidence.

Catch the overlap before you submit — Scope covers how the two lists interact.

What the seal does

  1. Finalise control ids. Each finding's framework control identifiers are resolved from the framework's ATT&CK and WSTG tables, and each artifact takes the union of the identifiers of the findings it is linked to.
  2. Build the chain and the manifest. The run's artifacts are read in sequence order and hashed into the chain; the manifest records every entry, the chain head, the entry count, the framework and the scope digest.
  3. Store the manifest. The manifest's exact bytes go into the content-addressed store under their own hash, so a later export ships the bytes that were hashed rather than a re-serialisation of them.
  4. Sign. A signature block is assembled holding the format version, the signature algorithm, the chain head, the manifest's hash, the signing key's id and the signing time. Those canonical bytes are signed with Ed25519 and stored alongside the manifest.
  5. Record the seal. One row per engagement holds the chain head, the manifest hash, the entry count, the algorithms, the signing key id, the signature and the timestamps.

The signature covers the block, not the manifest directly. That binds the key identity and the signing time into the signed material: a signature cannot be lifted onto a different manifest, and a key id that does not match the key is a failed check rather than a confusing one.

On success the engagement's progress stream carries a final event naming the chain head, the manifest hash, the entry count and the signing key id.

Fail-closed

If the key-encryption key is missing, or any step of the signature fails, sealing raises and no seal row is written. The run is left unsealed rather than recorded with a half-made seal. The failure is logged loudly; it does not mark the run failed, because the run is not what failed.

A scheduler started without a key-encryption key logs the problem and keeps running. It will accept jobs, run agents and persist results normally, and none of those runs will be sealed until the configuration is fixed.

There is no re-seal

Sealing is attempted once. Nothing in the product calls it again for a run that failed to seal: there is no admin endpoint, no CLI and no retry queue. A run whose seal attempt failed stays unsealed permanently, and fixing the configuration afterwards does not go back for it.

The function itself is idempotent — an existing seal is returned unchanged and never re-signed. There is no re-seal path.

After the seal

One seal exists per engagement, enforced in the database. Artifacts that arrive after a seal is recorded are ignored rather than appended, so the sealed set is fixed at the moment the seal is written.

An unsealed run is not lost. Its findings are stored and the report renders, and the PDF, SARIF and CSV exports all work, with no per-finding hashes. The PDF and SARIF state that the evidence is unsealed; the CSV has no column for it, as SARIF and CSV exports describes. A locker export of an unsealed run is refused.

Next: Exporting a locker.

Last updated 2026-09-24