Skip to content

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.

The mark sits on the property, beside the rules it keeps. Nothing else changes:

schemas/page.json
{
"$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.

A page carries the ciphertext, ~ and at least 82 base64url characters:

docs/auth.md
---
title: auth
owner: ~Ab0v7UwUrZNKLufd-hMeF5JHHA2hoNyUq77gIucl-mbytpkQt_C-tPNKZilTy1-khxyd3CcwMt6qVxbEJw
---

Four pages, four states, one run. The key is available for all of them:

Terminal window
$ manni meta validate
Using 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.

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:

Terminal window
$ manni meta validate
Using 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 1

Three things follow, and all three matter to whoever owns the schema.

  • encrypted:plain still 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.md and docs/wrong-key.md pass 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:

Terminal window
$ manni meta fill docs/plain.md --fields owner --dry-run
Using manni.config.yaml (.)
✓ docs/plain.md
/owner (encrypted) 1.00

A manni meta query UPDATE or INSERT writes a marked top-level property encrypted, resolving each file’s schema set the way validate does:

Terminal window
$ 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 — written

Reads do not decrypt. manni meta get and a SELECT both return what the page holds:

Terminal window
$ manni meta query "SELECT _path, owner FROM docs"
Using manni.config.yaml (.)
_path owner
docs/auth.md ~Ab0v7UwUrZNKLufd-hMeF5JHHA2hoNyUq77gIucl-mbytpkQt_C-tPNKZilTy1-khxyd3CcwMt6qVxbEJw
docs/bad-value.md ~Add2BdH6xfyELjVSH5kYwBbQ1_qkNGxwxWOulXucCSFbVi4zJJ7p_e_bDr8OJKo6nw27dPTj5ISVQPaSIA
docs/plain.md platform
docs/wrong-key.md ~AaE3zLy97i-pW5wvuARqsjGu1v4PbTxbyAL_WfkvNy2Mcq4cCeVeBBQwGR_vhn9cdo0sv-79oj3wP0HfDA
4 rows

So 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.

The findings ride the ordinary reporters, so nothing new has to be parsed. The schema name is the rule that fired:

Terminal window
$ manni meta validate -f github
Using 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.

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:

Terminal window
$ manni meta validate
Using 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 1

The 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:

Terminal window
$ manni key rotate
docs/auth.md: /owner ~Ab0v… -> ~ATsH…
1 value re-encrypted in 1 file, 0 skipped
Encryption key written to manni.config.yaml.
# exit 0

Rotation 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:

Terminal window
$ manni key rotate
docs/auth.md: /owner ~Ab0v… -> ~AYs_…
docs/bad-value.md: /owner ~Add2… -> ~Ae2_…
docs/wrong-key.md: /owner skipped: does not decrypt under the current key
2 values re-encrypted in 2 files, 1 skipped
Key not written: 1 value could not be re-encrypted. Fix it and rotate again.
# exit 1

Nothing 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.

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.