[Metadata encryption]

Fernet-encrypted LSP builtin_metadata.json — key lifecycle, CI secrets, integrity hashes, and runtime decrypt.

Metadata encryption

Abstract

LSP completion, hover, and signature help depend on a large builtin metadata document derived from the live evaluator surface. In open development the JSON is plaintext and git-tracked. For compiled Nuitka binaries, the build pipeline can ship an encrypted blob so the binary does not leave a trivially scrapable metadata file on disk.

Mechanism: Fernet (symmetric, cryptography package) + optional SHA256 integrity sidecar.

Conceptual model

Interface surface

Files

PathTracked?Role
src/pynescript/langserver/providers/builtin_metadata.jsonyesPlaintext for dev / hatch
…/builtin_metadata.json.encoften yesEncrypted payload for binaries
…/builtin_metadata.json.sha256yesTruncated SHA256 of plaintext
scripts/build/.metadata.keyno (gitignored)Local Fernet key material
scripts/generate_builtin_metadata.pyyesRegenerates JSON from code

Environment variables

VariableContextMeaning
CRYPTO_KEYBuild scripts / CI / Cloud BuildKey bytes for encrypt stage
PYNESCRIPT_METADATA_KEYRuntime decrypt fallbackEnv-supplied key if file missing
GitHub secrets.METADATA_KEYActionsStable secret → CRYPTO_KEY
Cloud Build _METADATA_KEYcloudbuild.yaml substitutionCRYPTO_KEY=${_METADATA_KEY}

Code entrypoints

  • Encrypt stage: scripts/build/compile.py (encrypt_metadata), scripts/build/ci_build.py (stage_metadata)
  • Decrypt: src/pynescript/langserver/providers/metadata_decrypt.py
  • Loader preference: plaintext if present, else encrypted (get_metadata_cached)

Internals

Generation (always code-derived)

python scripts/generate_builtin_metadata.py

Do not hand-edit the JSON for permanent feature work — re-run the generator after adding builtins.

Encrypt (local sketch)

from cryptography.fernet import Fernet
key = Fernet.generate_key()  # or load stable secret
fernet = Fernet(key)
encrypted = fernet.encrypt(plaintext_bytes)

Build scripts chmod the key file to 0o600 when writing .metadata.key.

Decrypt integrity check

After Fernet decrypt, if a .sha256 sidecar exists, the loader compares sha256(plaintext)[:16] and raises on mismatch.

Dev vs binary

ModeBehavior
Editable install / repo checkoutPrefer plaintext JSON
Nuitka onefileEncrypted data dir + key resolution via MEIPASS / env
Missing bothFileNotFoundError directing to compile script

Invariants & edge cases

  1. Stable key ⇒ reproducible .enc. Random key every CI run yields a different ciphertext even if plaintext is identical — not a functional bug, but pollutes diffs and caches.
  2. Plaintext remains in git for open-source transparency of the language surface. Encryption protects the bundled binary layout, not the secret of the API catalog.
  3. Truncated hash is integrity, not secrecy. It detects bitrot / wrong plaintext, not attackers with the key.
  4. Never commit .metadata.key. Rotate if leaked; regenerate .enc with the new key for binary builds.
  5. After adding builtins: regenerate metadata, re-encrypt with the same CI key, commit updated JSON (and .enc if tracked).

Worked examples

Regenerate + local encrypt

python scripts/generate_builtin_metadata.py
pip install cryptography
python scripts/build/compile.py --check   # stages metadata paths as configured

CI snippet

- name: Build LSP binary
  run: python scripts/build/ci_build.py --jobs 4
  env:
    CRYPTO_KEY: ${{ secrets.METADATA_KEY }}

Runtime with env key

export PYNESCRIPT_METADATA_KEY="$(cat scripts/build/.metadata.key)"
pynescript-lsp

Failure modes

SymptomCauseFix
No metadata decryption key foundMissing file + envSupply PYNESCRIPT_METADATA_KEY or rebuild with key
Metadata integrity check failedStale hash or wrong ciphertextRe-encrypt from current plaintext; commit both
Completion empty in binary onlyData dir not included in NuitkaFix --include-data-dir
Divergent .enc every CIUnset CRYPTO_KEYConfigure METADATA_KEY secret

See also