---
title: "Review data and privacy migrations"
description: "Keep a backup and erasure ledgers before applying changes to stored records and their visibility."
canonical_url: "https://docs.oz0.ai/fleet/upgrades/data-migrations"
---
# Review data and privacy migrations

> Keep a backup and erasure ledgers before applying changes to stored records and their visibility.

For: cluster operators.

Keep a backup and erasure ledgers before applying changes to stored records and their visibility.

## Error groups start again, and older failures lose their words

Rows written before this release kept what a failure said, in a job's summary, in its error group and in a continuation's report, and an error group's ID was a plain digest of those words. This release's first start removes them, in the migration, on every tenant:

- `oz0 job errors` and the console's error groups start empty, and fill again from the next failure. Their history is gone; the jobs themselves stay.
- An older job's error reads as its kind in a fixed sentence, for every reader, and a finding that quoted a failure keeps the sentence before the quote. A job with a failure kind you declared keeps that kind.
- A continuation's report counts its older failures under one fixed sentence, with the counts kept.
- `group:` searches find nothing older, as those jobs no longer have a group.

The words are still in the job's history for whoever may read them, for as long as the history is kept. Take a [backup](https://docs.oz0.ai/fleet/backups#back-up) first if you want the old rows for yourself: the migration cannot be undone. On a large database it reads every failed job's row once, in one transaction, so the first start takes longer.

## Works-council mode is on after the upgrade

The release with [works-council mode](https://docs.oz0.ai/govern/works-council-mode) turns it on for every tenant, those that existed before included. From then on, admins and readers no longer see other people's rows, or teams of fewer than five people, on the Cost page's *By user and team*, the Teams page and in `quota usage`: those show only the teams of five or more people, service identities, their own row and the tenant's total. Quotas keep being enforced. To see the per-person rows again, an admin turns the mode off for the tenant with a reason, which the audit log keeps:

```bash [Terminal]
oz0 tenant works-council acme --off --reason "agreed with the employee representatives on 2026-10-01"
```

## Deferred erasures name no article

From this release a `subject.erase.deferred` entry no longer carries the label `GDPR Art. 17(3)(b)`: it gives the reason `keep_for` and, when the agent's keep-for rule records one, the legal basis you recorded with `oz0 retention keep-for AGENT PERIOD --basis ID`, an ID from your tenant's own list of legal bases ([keep-for periods](https://docs.oz0.ai/govern/personal-data/retention#keep-what-a-law-asks-you-to-keep)). Entries written before keep their label. A script or a SIEM rule that matched the label should match `reason` and `basis` instead.

## Files between tools become their job tree's

From this release a file one tool hands another is readable only by the job tree that stored it, the later turns of
its conversation, and a job a person with the content permission hands it to ([who may read a
file](https://docs.oz0.ai/build/parts/tools/results-and-files#files-between-tools)); where payloads are encrypted, the edge seals it with the tenant's
key.

Nothing needs doing for files kept before the upgrade: they stay as they were, readable by any job of their
tenant, until the 90-day payload collection takes them, and are never rewritten in place (a tool that stores the same
bytes again stores them sealed and scoped). An erasure reaches such an older file only if it was handed to a job since.

A node on an older runtime sends no call binding with its file requests. Under `--call-binding when-present`, and
under the default `auto` from a node that itself runs an older runtime, the edge takes them as before, but they read
only those older files and the files other older runtimes stored, and what they store is readable by any job of the
tenant.

Such a file names no job, so an erasure does not reach it: it goes only with the 90-day payload
collection.

Upgrade the nodes' runtimes soon after the servers, before you rely on erasure reaching files.

`auto`
refuses file requests without a binding from every node whose runtime sends them, as it refuses model calls; under
`--call-binding required` the edge refuses them from the start.

A job that
named another job's file, such as a script that runs `oz0 tool call` and then `oz0 run` with the file, keeps working
when the caller may see content; an app key without the content permission is refused when it names one.

A start
under a job ID you choose (`oz0 run --id`, `oz0 tool call --id`) is refused once the job that had that ID has ended
while its files are still kept, or when the ID is a conversation's turn, since the new job would read them: choose a
new ID for each run, or leave the ID out.

With the roles apart, an edge whose management is away refuses a start under
a job ID you choose, as unavailable, since management keeps whether the ID is a conversation's turn: an app that
starts jobs while management may be down leaves the ID out.

## Memory records get a kind, a purpose and an end

Records plugins kept before this release count as about people when their job tree had a data subject and as work
otherwise, have no purpose, and end 90 days after the upgrade unless renewed; a record a newer one replaced is closed
([Personal data](https://docs.oz0.ai/govern/personal-data)). Memory about people is off for every agent after the upgrade: turn it on per
agent for a purpose it declares (`oz0 memory person <agent> --purpose <purpose> --reason ...`, a governance change)
before agents that remember people need it again.

## Audit exports: `--key` and `--complete` take a signed head

Every audit export's head is now signed with the audit key, not only one with blinded entries, so a plain export reads the key, and the first one can create it. `oz0 audit verify --file` with `--key` or `--complete` fails on a file with no signed head: export it again.

`--complete` now also needs `--key`, or `--unpinned` to accept that it shows only that the file is whole, and the output says `complete` only when `--key` pinned the signature.

Scripts that pass `--complete`, `--key` or `--unpinned` without `--file`, `--tenant` or `--cluster` with it, or a `--key` from a variable that may be empty, now fail instead of checking less than they say; see [Export](https://docs.oz0.ai/govern/audit-trail#export).

## Erasures are recorded in the ledger or refused

An erasure is now written to the [erasure ledger](https://docs.oz0.ai/govern/personal-data/retention#backups) in the same transaction that applies it, and its line is anchored in the database. A ledger that cannot be written refuses the erasure ("nothing was erased"), and so does a ledger that lost lines the database knows of; `oz0 doctor` fails its erasure ledger check with the reason.

A ledger kept before this release may hold two lines glued into one by an append that failed; it does not verify, and erasures are refused until you start a new ledger in another file (`--erasure-ledger`) and keep the old one for restores (`server restore` takes `--erasure-ledger` more than once).

## The erasure ledger on a volume of its own

The Helm chart and `docker compose` now keep management's erasure ledger on a volume that outlives the pod or container, apart from the database: before, it was in the data directory, which a rollout or `docker compose up` with new images replaced, and from this release an erasure is refused while the ledger lacks lines the database knows of.

- Helm: each server gets a volume claim `erasure-ledger` (`erasureLedger.persistence`). A StatefulSet cannot gain a volume claim template in place, so replace it once, leaving its pods running: `kubectl -n oz0 delete statefulset oz0-orchestrator-zero-server --cascade=orphan`, then `helm upgrade`. With `roles.separate`, set `erasureLedger.existingClaim` to a ReadWriteMany claim. The old ledgers in the pods' data directories are gone already; the new ledger starts empty, under a new name, so erasures are not refused.
- Compose: `server` writes `/oz0/ledger/erasures.jsonl` in the new volume `ledger`; `docker compose up -d` creates it.

A server that keeps the ledger at its default in a container's data directory says so in its log, and doctor warns.

## Subjects are matched by their normal form

A data subject is now hashed in its [normal form](https://docs.oz0.ai/govern/personal-data), so spellings of one ID reach the same person: a mail address in any case or with `mailto:`, a phone number with `+` or `tel:` and any separators, fullwidth characters, accents written either way, invisible characters. Other IDs keep their case and separators.

Jobs recorded before the upgrade under a spelling that is not in normal form keep the hash of that exact spelling: `subject erase`, `subject find`, `subject export`, `subject decisions`, a restriction or an objection and its lift by that spelling still reach them, and a start checks restrictions under both.

## Nodes look only for secrets a guess would not find

The fingerprints nodes scan tool calls and requests for leave out values shorter than 16 bytes and values built the way people build passwords (words of any language or script, names, keyboard runs and zigzags, sequences and two of them woven together, years, dates and times, repeats), since whoever holds a node's fingerprints could find those by guessing ([tool policy](https://docs.oz0.ai/govern/tool-policy/runtime#calls-that-carry-a-secret)). Setting such a secret says so. Replace a human-made value with a random token of 16 bytes or more to have it looked for.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
