ai.oz0/public-access
For: tenant administrators and plugin users.
ai.oz0/public-access helps a public authority disclose and archive the records its agents leave. Give it a tenant export, or a subject export containing one person's records.
Before reading the export, the plugin verifies its signatures and every file hash. It refuses an export whose content changed.
- A public-access copy. It proposes what to mask in the records a request is for, a person decides each proposal, approves the review, and the plugin writes the masked copy. It never decides what is secret, and it never hands anything out.
- An E-ARK archive package. It builds a Submission Information Package (SIP) of the export, to the European E-ARK specifications that e-archives accept, and checks any E-ARK package against their rules.
It reaches no network, needs no secret, and asks the cluster for nothing. The records never go to a model.
Install the command
The steps run where the export is, on the registrar's machine, offline:
uv tool install ./plugins/public-access # from a checkout of the repository, or the version the hub pins
oz0-public-access --version
The plugin also installs in a tenant like any other (oz0 plugin install hub:ai.oz0/public-access), which gives you
the same steps as the functions of the tool oz0_public_access: see In the cluster.
Answer a request: export, mask, review, release
- Export the records. Make a tenant export with the jobs' content, for the period the request is about, and
note the audit key's fingerprint, which you pin in every step:Terminal
oz0 tenant export acme --content --since 2026-09-01T00:00:00Z --out ./export oz0 audit key # the fingerprint to pin oz0-public-access verify ./export --key 3f9a… # signatures and every file's hash oz0-public-access verify ./export --key 3f9a… --files # what the export holds
Making an export needs the tenant'stenant.exportpermission and, for the jobs' content, the content permission. Someone else's private chat is exported without what was said unless you give a reason, which is audited. A subject export, one person's copy of their records, is read the same way. - Propose what to mask. Name the records the request is for, by job or by file of the export:Terminal
oz0-public-access propose ./export --key 3f9a… --job job-123 --job job-456 \ --table job_board --names ./names.txt --out ./review
Every string value, every object key and every number in each record is scanned (a personnummer stored as a number, an e-mail address used as a key), and every candidate goes intoreview/review.jsonand a readablereview/review.html, pending, with its place: the record, a JSON pointer, and whether it is a key or a number. Texts are compared in Unicode NFC, so a name typed with combining marks is the same name:Candidate Found by personnummer,samordningsnummerSwedish personal identity and coordination numbers, with their date and control digit email,phone,iban,payment cardThe guardrails plugin's baseline personal-data patterns nameEach name of the list you give with --names(one per line), whole words or in the genitive (Annas), any casefree textA string of 200 characters or more, or of two sentences or more: read it, since what was said is where a secret most often is
Other baseline packs (--pack-id pii.no,pii.dk,pii.fi,pii.is,pii.de) and packs of your own in the guardrails plugin's pack format,oz0.guardrails.pack/v1(--pack FILE, such as your diary numbers) add patterns. The patterns can miss things and find things that are not secret: they are suggestions. - Decide each item. Read
review.html, then:Terminaloz0-public-access review ./review list --pending oz0-public-access review ./review set c00001 c00004 --mask --ground "OSL 26 kap. 1 §" oz0-public-access review ./review set c00002 --keep # an official's work e-mail, handed out oz0-public-access review ./review set --all-pending --kind free_text --read oz0-public-access review ./review add ./export --key 3f9a… --text "the neighbour's diagnosis" \ --ground "OSL 25 kap. 1 §" # what the rules missed, masked wherever it isaddfinds the text as a whole word, in any case, in values, keys and numbers:--text PermasksPerandPER, never the start ofpermission.
A mask needs a ground in your own words (the plugin suggests none);readsays you read a free-text item and there is nothing more to mask in it;keephands the text out as it is. - Approve. The approval names who approved and the decision's reference, such as its diary number:Terminal
oz0-public-access approve ./review --by "Rut Registrator" --decision-ref "KS 2026/1042"
It is refused while any item is pending. It binds the SHA-256 of every decision: change one afterwards, with the command or by hand, and the approval is void. It records a person's act; it is not a signature: whoever can write the review folder can approve it, so keep the folder where only the registrars can. - Release. Write the copy you hand out:Terminal
oz0-public-access release ./export --key 3f9a… --review ./review --out ./copycopy/records/holds the requested records with each masked place replaced by[MASKED n]: a masked key is renamed[MASKED n], and a masked number becomes the string"[MASKED n]".copy/release.jsonlists eachnwith its record, its place in the copy (a JSON pointer, with masked keys already masked) and the ground, and says who approved it, when and under which reference. The masked text is in no file of the copy. Release refuses when:- the review is not approved, or changed after it was approved;
- it was made on another export, or a record changed since (the export is verified again);
- a masked text still appears anywhere in the copy: the same personnummer kept in another field or record,
written another way (
811218 9876or19811218-9876for811218-9876) or as a number, a name kept in a JSON key or broken over a line, a masked paragraph repeated in another step's output, or a masked text in a ground you typed.
Every item you mask counts, also one inside a larger text you masked whole: a personnummer in a masked paragraph is still looked for in the other fields.
The check reads the decoded texts (values, keys and numbers), so a text with line breaks or quotes is found too, and it matches whole words in any case: a maskedPeris found inPersandPER, not inpermission. The refusal says where, never what; mask those places too, or keep the text everywhere.
Release sends nothing anywhere. Handing out the copy, and the decision that goes with it, with its appeal information, are the authority's.
The review folder holds the records' text: keep it as you keep the records.
Archive: an E-ARK package
oz0-public-access archive ./export --key 3f9a… --submitter "Exempel kommun" --submitter-code 212000-0000 \
--creator "Socialnämnden" --reference-code "SE/EK/SN" --submission-agreement "LA 2026:1" --tar sip.tar
oz0-public-access check sip.tar
The package follows the Common Specification for Information Packages (CSIP) 2.2.0 and the SIP specification 2.2.0 of E-ARK, with PREMIS 3.0 preservation metadata:
| Path | What it holds |
|---|---|
uuid-…/ | One root folder, named after the package's ID |
METS.xml | The package's METS: the SIP profile, the submitting agent, the archival creator, the submission agreement and reference code, the files with their sizes and SHA-256, and the structure |
metadata/descriptive/dc.xml | A Dublin Core description |
metadata/preservation/premis.xml | Every file with its SHA-256, size and format; the export's signature check (the key's fingerprint, and whether you pinned it) and the package's creation as events |
representations/rep1/METS.xml, representations/rep1/data/ | The export's files byte for byte, its manifests, signatures and keys included, so the archive keeps the proof |
documentation/ | What the package holds, and with --release ./copy the copy's release.json |
schemas/ | The export manifest's shape as a JSON Schema |
--record PATH archives only some files of the export (with its manifests and keys); --out FOLDER writes the
folder instead of a tar. Every package is checked before it is written. oz0 tenant export --verify representations/rep1/data --key … checks the archived export again with the core.
check reads any E-ARK package, a folder or a tar, offline, and fails on what the specifications say a package must do: one root folder; the package and representation METS documents (their ID, content category, profile, header, software agent, the submitting agent, metadata references, file groups for documentation, schemas and representations, each file's size and checksum, its location inside the package, the CSIP structural map and its pointers); and each PREMIS file object's fixity. What a package should do is a warning.
It reads XML only without a DOCTYPE, so no entity is expanded, and fetches nothing: no schema, no outside validator. It reads and hashes each file once, however many references name it, and lists at most the first 1,000 findings. It does not check an authority's own profile (such as the National Archives' FGS packages) or what an archive must hold: ask your e-archive.
In the cluster
The tool oz0_public_access has the same steps as functions. Each takes the export as a
file between tools (an export's tar, at most 32 MiB) and returns
what it makes as a file of its own, so the records' text never reaches a model or a job's history: results carry
counts, paths, item IDs and rule names only.
| Function | What it does |
|---|---|
oz0_public_access.rules | The masking rules in effect: the packs and their patterns, and how many names the list holds (never the names) |
oz0_public_access.verify | export, key: the export's checks, as verify |
oz0_public_access.propose | export, key, records, jobs: the review as a file, every item pending |
oz0_public_access.archive | export, key, submitter, creator, reference_code, submission_agreement: the SIP as a tar file |
oz0_public_access.check_archive | package: the check's errors and warnings |
There is no release function in the cluster. People cannot fetch a file from the cluster or hand one back yet, so a review that reached a release call in a flow would have been decided and approved by a tool, and a person asked to approve that call would see only file hashes, never one decision. The masked copy is therefore written only by the command, from a review a person decided and approved on their own machine. Release joins these functions once a person can fetch the proposed review and hand the decided one back.
Files between tools are kept on the edge for 90 days after a tree last stored them. New files are sealed with the tenant's payload key where payload encryption is on, as it is by default in a cluster. Files stored before file scoping was introduced remain plain until rewritten.
A review file holds the records' text. Use the command when those records must stay off the cluster. These functions serve a flow whose earlier step produced the export as a file; the walkthrough above needs none of it.
A tar given to them may unpack to at most 1 GiB, and the command's to 64 times the tar's size (at least 256 MiB); for a larger export, unpack it and give the folder.
| Setting | What it is for |
|---|---|
PUBLIC_ACCESS_NAMES (a secret) | The names to look for, one per line: people whose names are themselves to be protected, so a secret |
PUBLIC_ACCESS_BASELINE | The baseline packs, separated by commas; empty: pii.core and pii.se |
PUBLIC_ACCESS_PACKS | More packs, as the guardrails plugin's GUARDRAILS_PACKS |
The command
| Command | What it does |
|---|---|
verify EXPORT --key FP [--files] [--json] | Check an export's signatures and every file's hash |
rules [--pack-id ID] [--pack FILE] [--names FILE] [--json] | List the masking rules in effect |
propose EXPORT --key FP (--job ID | --record PATH | --table NAME)... --out REVIEW | Propose what to mask: a review, every item pending |
review REVIEW list [--pending] [--json] | List the items |
review REVIEW set ID... (--mask --ground G | --keep | --read | --pending) [--all-pending [--kind K]] [--note N] | Decide items |
review REVIEW add EXPORT --key FP --text T --ground G [--record PATH] [--pointer P] | Mask text the rules missed, wherever it is |
approve REVIEW --by NAME --decision-ref REF | Approve every decision |
release EXPORT --key FP --review REVIEW --out COPY | Write the masked copy |
archive EXPORT --key FP --submitter ORG (--out FOLDER | --tar FILE) [...] | Build an E-ARK SIP |
check PACKAGE [--json] | Check an E-ARK package |
--trust-export-key instead of --key trusts the key the export carries, and every result says so. Exit status: 0
done or passed, 1 refused or failed (the reason on standard error), 2 used wrongly.
Tests
The plugin has no agent and no flow, so it has no evals to replay and no quality gate to pass: nothing in it calls a
model. make test-plugins runs its tests (signatures and tampering, the rules, the review and its refusals, the
E-ARK rules one by one, the MCP functions over files), and make e2e-public-access runs the walkthrough on a dev
server's real export: verified with the pinned key, refused when changed, masked by a person's decisions, archived and
checked, and the plugin's server answering on a node.
ai.oz0/setup-assistant
An agent in the chat drawer that explains your tenant to you, plans changes you approve, and drafts tool rules from plain English into shadow mode.
ai.oz0/email
Agents send mail for their jobs to the tenant's own domains, with the AI label and approval by default, through the operator's SMTP relay or Microsoft Graph, every message recorded.