Require a field and keep its value private
Some fields your standard requires are not for publishing. An owner, an
internal ticket, the name of a service. You still want every page to carry one,
and you still want the value checked against the rules you wrote.
x-manni-encrypt is the keyword for that. The page holds a ciphertext, and
validation checks the real value wherever the key is available. This page
follows one such value from the schema through every command that touches it.
Everything here assumes a key already exists. Put the key where every tool finds it is where that decision is made.
Every transcript below is real output from the built tool.
Mark the property
Section titled “Mark the property”The mark sits on the property, beside the rules it keeps. Nothing else changes:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["title", "owner"], "properties": { "title": { "type": "string" }, "owner": { "type": "string", "enum": ["platform", "billing"], "x-manni-encrypt": true } }}owner is still required, and its enum still holds. What the mark adds is
that a page carrying the word platform in the open now fails.
The mark counts wherever the validator evaluates it. That includes a $ref, an
allOf, a referenced built-in, and the anyOf, oneOf and if/then
branches the validator takes. A mark behind a failing if does not count. A
value other than true or false is a schema error at compile time, exit 2.
The schema resolution
reference has the
rules in full.
Follow the value
Section titled “Follow the value”A page carries the ciphertext, ~ and at least 82 base64url characters:
---title: authowner: ~Ab0v7UwUrZNKLufd-hMeF5JHHA2hoNyUq77gIucl-mbytpkQt_C-tPNKZilTy1-khxyd3CcwMt6qVxbEJw---Four pages, four states, one run. The key is available for all of them:
$ manni meta validateUsing manni.config.yaml (.)✓ docs/auth.md✗ docs/bad-value.md /owner must be equal to one of the allowed values (line 3) [./schemas/page.json]✗ docs/plain.md /owner /owner holds a plain value; its schema marks it x-manni-encrypt. (line 3) [encrypted:plain]✗ docs/wrong-key.md /owner /owner does not decrypt under the current key: encrypted under another key, or edited by hand. (line 3) [encrypted:unreadable]
4 files checked, 1 passed, 3 failed, 3 errors# exit 1| Page | What it holds | What validation does |
|---|---|---|
docs/auth.md |
A ciphertext that decrypts to platform |
Validates the decrypted value against the property’s whole schema, and passes |
docs/bad-value.md |
A ciphertext that decrypts to finance |
Fails on your enum, under your schema’s name, without printing either value |
docs/plain.md |
The word platform |
Fails encrypted:plain, whatever the key state |
docs/wrong-key.md |
A ciphertext from another key | Fails encrypted:unreadable |
The second row is the one worth dwelling on. enum, pattern, format and
the rest all apply to the real value, so the standard you wrote is enforced on
a value the page never shows. No finding ever prints the plaintext or the key.
Validation works on a copy of the metadata with every readable value decrypted. The page itself is never rewritten.
With no key, the check goes quiet
Section titled “With no key, the check goes quiet”Findings at or under an encrypted value are dropped, and one warning on stderr counts them. The exit code is not affected by what was dropped:
$ manni meta validateUsing manni.config.yaml (.)manni: 3 encrypted values were not verified: no encryption key is available. Set MANNI_ENCRYPTION_KEY, or run `manni key set`.✓ docs/auth.md✓ docs/bad-value.md✗ docs/plain.md /owner /owner holds a plain value; its schema marks it x-manni-encrypt. (line 3) [encrypted:plain]✓ docs/wrong-key.md
4 files checked, 3 passed, 1 failed, 1 error# exit 1Three things follow, and all three matter to whoever owns the schema.
encrypted:plainstill fires. No key is needed to see that a value is not a ciphertext, so the rule you care about most keeps working everywhere.- A wrong value and a wrong key both go silent.
docs/bad-value.mdanddocs/wrong-key.mdpass a run that cannot read them. - A baseline written without the key records none of the dropped findings. They return as new on the first run that has it.
What writes a ciphertext, and what does not
Section titled “What writes a ciphertext, and what does not”Two commands write a marked value, and both report (encrypted) in place of
it. manni meta fill encrypts a valid value the page already holds in plain
text, with no model call:
$ manni meta fill docs/plain.md --fields owner --dry-runUsing manni.config.yaml (.)✓ docs/plain.md /owner (encrypted) 1.00A manni meta query UPDATE or INSERT writes a marked top-level property
encrypted, resolving each file’s schema set the way validate does:
$ manni meta query "UPDATE docs SET owner = 'platform' WHERE _path = 'docs/auth.md'"Using manni.config.yaml (.)docs/auth.md: owner: (unset) -> (encrypted)✓ 1 change across 1 file — writtenReads do not decrypt. manni meta get and a SELECT both return what the page
holds:
$ manni meta query "SELECT _path, owner FROM docs"Using manni.config.yaml (.)_path ownerdocs/auth.md ~Ab0v7UwUrZNKLufd-hMeF5JHHA2hoNyUq77gIucl-mbytpkQt_C-tPNKZilTy1-khxyd3CcwMt6qVxbEJwdocs/bad-value.md ~Add2BdH6xfyELjVSH5kYwBbQ1_qkNGxwxWOulXucCSFbVi4zJJ7p_e_bDr8OJKo6nw27dPTj5ISVQPaSIAdocs/plain.md platformdocs/wrong-key.md ~AaE3zLy97i-pW5wvuARqsjGu1v4PbTxbyAL_WfkvNy2Mcq4cCeVeBBQwGR_vhn9cdo0sv-79oj3wP0HfDA4 rowsSo a WHERE owner = 'platform' matches only the page that failed the check.
Equal values do encrypt to equal ciphertexts, so comparing one page’s column
with another’s still finds the pages that share an owner. The query
reference has the rest.
A nested mark is validated and never written. Only top-level properties are written encrypted.
What a reviewer sees
Section titled “What a reviewer sees”The findings ride the ordinary reporters, so nothing new has to be parsed. The schema name is the rule that fired:
$ manni meta validate -f githubUsing manni.config.yaml (.)::error file=docs/bad-value.md,line=3::[./schemas/page.json] /owner must be equal to one of the allowed values::error file=docs/plain.md,line=3::[encrypted:plain] /owner /owner holds a plain value; its schema marks it x-manni-encrypt.::error file=docs/wrong-key.md,line=3::[encrypted:unreadable] /owner /owner does not decrypt under the current key: encrypted under another key, or edited by hand.| Format | What an encrypted finding looks like |
|---|---|
pretty |
The transcripts above. The field is the pointer, /owner, and the bracket holds encrypted:plain or encrypted:unreadable. |
json |
An entry in errors[] with "keyword": "encrypted" and "schema": "encrypted:plain". No severity, so it is an error. |
github |
::error file=…,line=3::[encrypted:plain] /owner …, as above. |
sarif |
"ruleId": "encrypted:plain/encrypted" at "level": "error". |
junit |
<failure type="encrypted:plain/encrypted" message="/owner /owner holds a plain value; its schema marks it x-manni-encrypt. (line 3)"/> |
Every one of them is an error. The only warnings manni meta validate emits
are the location:external and location:page findings. Severity across the
family
says which tools emit the lower two.
No plaintext and no key appears in any of these. That is what makes the SARIF log safe to upload from a public job.
A value the manifest supplies
Section titled “A value the manifest supplies”An external-metadata manifest is the other place a private value lives. A manifest is private by construction, so a plain value from one is validated as plain text and never flagged.
An encrypted value from one follows the page rules exactly. It decrypts, it is checked against your schema, and a finding names the manifest and the line inside it:
$ manni meta validateUsing manni.config.yaml (.)✓ docs/auth.md✗ docs/stale.md /owner /owner does not decrypt under the current key: encrypted under another key, or edited by hand. (owners.yaml:9) [encrypted:unreadable]✗ docs/wrong.md /owner must be equal to one of the allowed values (owners.yaml:7) [../encrypted.schema.json]
3 files checked, 1 passed, 2 failed, 2 errors# exit 1The location is owners.yaml:7, not a page line, because that is where the
value is. A reviewer fixes it in the manifest.
What a rotation means for the schema you own
Section titled “What a rotation means for the schema you own”Nothing, which is the point worth stating. A rotation replaces every ciphertext
and changes no schema. Your enum still lists the same words, and a page that
passed before passes after:
$ manni key rotatedocs/auth.md: /owner ~Ab0v… -> ~ATsH…1 value re-encrypted in 1 file, 0 skippedEncryption key written to manni.config.yaml.# exit 0Rotation finds values by their ciphertext rather than by your marks. A mark
behind a remote $ref the run cannot reach still cannot hide a value from it.
One thing does reach back to you. A value that decrypts under neither key stops the whole rotation, and it is named:
$ manni key rotatedocs/auth.md: /owner ~Ab0v… -> ~AYs_…docs/bad-value.md: /owner ~Add2… -> ~Ae2_…docs/wrong-key.md: /owner skipped: does not decrypt under the current key2 values re-encrypted in 2 files, 1 skippedKey not written: 1 value could not be re-encrypted. Fix it and rotate again.# exit 1Nothing was written, not even the two pages that re-encrypted cleanly. Whoever
runs the rotation will come to the schema’s owner with that page, because
deciding what its owner should have been is your call.
Choose which fields to mark
Section titled “Choose which fields to mark”Equal values encrypt to equal ciphertexts. That is deliberate, and it is what lets a manifest join on one and a query group by one. It also leaks on a small vocabulary.
An owner constrained to two words gives every page one of two ciphertexts. One
page whose owner is public knowledge then identifies every page that shares it.
Mark fields whose values are many and uneven, and think twice about marking a
two-value enum.