Metadata-Version: 2.3
Name: pyencode-protector
Version: 0.7.0
Summary: Protect CPython applications with authenticated encrypted module bundles
Description-Content-Type: text/markdown
Requires-Python: >=3.14,<3.15
Requires-Dist: cryptography>=42
License: MIT

# PyEncode

PyEncode protects CPython applications on Windows, Linux, and macOS. It compiles
Python modules to code objects, serializes them with `marshal` for the portable
runtime or a tagged primitive codec for the native runtime, compresses
them, and authenticated-encrypts them with AES-256-GCM. The generated runtime
decrypts and executes code in memory; neither plaintext source nor plaintext
bytecode is written to a temporary file.

Version 0.7.0 retains the PYE2/manifest-format-3 compatibility contract, adds
encrypted eligible statically spelled attribute names at `--obf-code 2`, and
introduces the licensed reusable-runtime contract. The initial exact native
targets are standard CPython 3.14 on Windows x64 (`cp314-win_amd64`) and the
publisher-built macOS Apple Silicon target (`cp314-macosx_arm64`). The independent
hardening controls are:

- `--obf-code 0` decrypts each module once when it is imported. `--obf-code 1`
  additionally puts the original code of module-level functions, methods, and
  lambdas in build-time authenticated capsules. The visible functions expose
  signature-preserving dispatch stubs while idle and restore their real `CodeType`
  only for a call. Nested definitions remain protected inside their
  encrypted parent and are sealed when that parent creates them. `--obf-code 2`
  includes level 1 and transforms eligible statically spelled attribute loads,
  stores, and deletes into authenticated AES-GCM name capsules dispatched by the runtime
  protector. This matches the documented meaning of PyArmor's current
  `--obf-code 2` option—hiding names in attribute chains—not its separate RFT,
  BCC, string-mixing, private/restrict, or anti-debugging features.
- `--runtime-backend python` emits the portable Python source runtime.
  `--runtime-backend bytecode` emits the same runtime as five adjacent,
  sourceless `.pyc` files bound to the build interpreter. The licensed
  `--runtime-backend native` flow copies one publisher-built, signed, immutable
  `pyencode_runtime._runtime` extension. Every application still gets a fresh
  master key, which is wrapped to the selected `runtime_id` and authorized by a
  signed build permit. Application modules remain encrypted `.pye` artifacts.
- `--native-module` is a separate application AOT option. Use it only when selected
  application modules themselves should become Cython/Zig `.pyd` extensions; it is
  not implied by a native runtime.

The reusable native backend always uses `--obf-code 2`; it rejects levels 0 and 1
instead of weakening or rebuilding the runtime. Portable Python and bytecode
remain explicit, separate backends. Native mode never falls back to either one
when an asset, target, license, or permit is missing or invalid.

The runtime-family protocol and publisher pipeline are specified in
[Reusable native runtime architecture](docs/RUNTIME_PACKS.md). No production
Authenticode- or Developer-ID-signed runtime asset or production permit service is
published by this repository yet, so this document does not claim that the
licensed native distribution is currently available to customers.

The existing protection contract also includes:

- Every artifact receives a random 128-bit filename, for example
  `myapp/7b82a40e9aa14e60db7094f2de3fbc33.pye`.
- PYE2 has no plaintext JSON header; an artifact contains only its magic value,
  nonce, and ciphertext/authentication tag.
- Module names, the entry point, and package flags are stored in an encrypted module
  index.
- Manifest format 3 is signed with Ed25519 and contains the SHA-256 digest of every
  artifact.
- The runtime, launcher, support trees, and resources copied by default are signed,
  key-bound, and verified again during `install()`. Explicit `--unsigned-data`
  resources are isolated from that trusted inventory for host-managed mutable data.
- The loader exposes only a small trampoline to `runpy`, rather than returning the
  application's real code object. Module keys are derived on demand and are not
  retained by the finder.
- Application modules explicitly selected with `--native-module` are removed from
  the encrypted index, emitted as signed
  ABI-tagged `.pyd` files, and recorded in signed `.pyencode-native.json` coverage.
- A dedicated finder rejects preloaded/shadowed native modules and rechecks the
  exact extension hash immediately before CPython loads it.

The goal is to make static analysis substantially more expensive than it is for
`.pyc` files. PyEncode does not claim to make reverse engineering impossible.

## Supported Python versions

The protected PyPI package for PyEncode 0.7.0 is published as two native wheels
for **standard CPython 3.14 only**: Windows x64 and macOS Apple Silicon. `pip`
selects the matching wheel automatically; the Windows and macOS binaries are not
combined into one file. Linux, Intel macOS, free-threaded CPython, PyPy, and other
Python versions cannot install these protected 0.7.0 wheels.

The source checkout and portable generated runtime still target standard CPython
3.10 or later. CI monitors Python 3.10 through 3.15, but only CPython 3.14 is a
release target until the corresponding native runtime families have been built
and validated for more ABIs.

The portable runtime source is Python-only. The default `cryptography` backend
uses that package's native extension; `--runtime-crypto pure-python` removes the
runtime dependency and is intended for restricted embedded hosts such as Fusion
on macOS.
The bytecode runtime avoids shipping those runtime sources and does not load a
Mach-O/PE extension, but `.pyc` is reversible and does not protect the embedded
key as strongly as the reusable native runtime.
However, a built artifact is tied to the exact CPython major/minor version used to
build it:

- an artifact built with CPython 3.11 runs on CPython 3.11;
- that artifact does not run on CPython 3.10, 3.12, or 3.14;
- to support several minor versions, build a separate output with each interpreter.

If a bundle includes `.pyd`, `.so`, `.dll`, or other native dependencies through
`--support`, the whole bundle is also constrained by the operating system,
architecture, and ABI of those files. Current testing targets the standard GIL
build of CPython; free-threaded and debug ABIs require separate builds and testing.

Each initial reusable native family supports one exact standard-GIL CPython 3.14
target: Windows x86-64 uses `_runtime.cp314-win_amd64.pyd`, while macOS Apple
Silicon uses `_runtime.cpython-314-darwin.so` and the `cp314-macosx_arm64` asset
target. A native build fails closed if the signed catalog, runtime hash/size,
`runtime_id`, ABI, license, or permit does not match. It does not compile a
replacement or silently select a portable backend. Application modules selected
with `--native-module` remain a separate Windows-only advanced feature and still
have their own toolchain needs.

On Linux, or on a Mac for which no matching publisher-signed runtime asset is
available, select `--runtime-backend python` or `bytecode` explicitly. Building and
signing the common macOS runtime is a publisher release operation described below;
ordinary PyEncode customers do not run it. CPython 3.10 and later remain supported
by the portable runtime, subject to the exact major/minor artifact binding
described above.

## Installation

Install the published package from PyPI with standard CPython 3.14. On Windows:

```powershell
py -3.14 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pyencode-protector
```

On an Apple-Silicon Mac, use an arm64 CPython 3.14 environment:

```bash
python3.14 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pyencode-protector
```

The wheel contains a small public bootstrap plus a level-2 encrypted payload.
It uses the prebuilt native runtime for the selected platform; installing the
package does not compile or sign a new `.pyd`/`.so`.

The CLI command and import namespace are both named `pyencode`.

For source development, use an editable installation in a virtual environment.

Windows PowerShell:

```powershell
py -3.11 -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e .
```

Linux:

```bash
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .
```

The builder depends on `cryptography>=42`. Portable generated applications use it
at runtime by default. Pass `--runtime-crypto pure-python` to emit a dependency-free
portable runtime that verifies Ed25519 signatures and opens the existing
AES-256-GCM PYE2 format using signed Python code. This compatibility backend is
slower and is not constant-time; it is intended for small protected bundles in
embedded hosts, not as a general-purpose cryptography library. The native runtime
has no generated-application dependency on `cryptography` or
`_pure_crypto.py`.

Licensed customers do not need a compiler or signing tool to use the reusable
native runtime. The compiler toolchain and platform code-signing credentials
belong to the publisher release pipeline. They are not used to customize the
common runtime on a customer machine.

## Usage

Built-in command-line help is available without opening the documentation:

```bash
pyencode --help
pyencode build --help
python -m pyencode --help
```

Protect a single file:

```bash
pyencode build hello.py -o dist/hello
python dist/hello/run.py
```

Protect a package containing `__main__.py`:

```bash
pyencode build src/myapp -o dist/myapp
python dist/myapp/run.py
```

Specify the entry module explicitly:

```bash
pyencode build src -o dist/app --entry myapp.__main__
```

Emit a CPython-bound sourceless runtime without native binaries:

```bash
pyencode build src -o dist/app --entry myapp.main \
  --obf-code 1 \
  --runtime-backend bytecode \
  --runtime-crypto pure-python
```

This mode emits `__init__.pyc`, `_mp_main.pyc`, `_runtime.pyc`,
`_pure_crypto.pyc`, and `_build.pyc` directly inside `pyencode_runtime`. Build it
with the same CPython major/minor used by the target host.

### Reusable native runtime (licensed flow)

The initial native targets consume a publisher-produced asset directory containing
one exact runtime binary -- `_runtime.cp314-win_amd64.pyd` for Windows x64 or
`_runtime.cpython-314-darwin.so` for macOS arm64 -- and its canonical, vendor-signed
`runtime-asset.json`. The metadata binds the immutable binary's filename, SHA-256,
size, ABI, `runtime_id`, RSA-3072 wrapping public key, and Ed25519 permit issuer.
A representative Windows licensed build is:

```powershell
pyencode build src -o dist/app --entry myapp.main `
  --obf-code 2 `
  --runtime-backend native `
  --runtime-asset C:\ProgramData\PyEncode\runtime-cp314-win_amd64 `
  --runtime-vendor-key C:\ProgramData\PyEncode\vendor-ed25519.pub `
  --runtime-id windows-cp314-amd64-v1 `
  --permit-url https://license.example.com/v1/build-permit
```

Set `PYENCODE_LICENSE_KEY` in the environment rather than placing the builder
license in shell history. The CLI verifies the vendor signature and exact asset
bytes, creates a fresh 32-byte application master key and `build_id`, and encrypts
the source locally. It wraps only that key with RSA-3072 OAEP using SHA-256 and
binds the permit to `application_commitment_sha256`, the domain-separated SHA-256
of the complete canonical manifest payload. The CLI sends the exact canonical
permit payload plus the license credential to the HTTPS permit service. The
service returns an Ed25519-signed permit stored as
`pyencode_runtime/_seal.json`. Application source and the plaintext master key are
never sent to the service.

`_seal.json` is mandatory and authenticated independently by the permit issuer.
It is deliberately absent from the manifest's `integrity.files`: putting the
permit inside the manifest commitment that the permit itself signs would create a
hash cycle. At startup the native runtime verifies the canonical permit and issuer
signature, verifies the manifest signature, recomputes the full application
commitment, and compares it with the permit before unwrapping or using the
application master key. The seal remains a required member of the immutable
distribution inventory even though it is outside `integrity.files`.

The runtime `.pyd` or `.so` is copied byte-for-byte so its publisher signature
remains valid. Customers do not build, patch, rename, or re-sign it. If the exact
asset, vendor key, `runtime_id`, target, license, or signed permit cannot be
verified, the build fails; native mode never compiles a local substitute or
downgrades to Python or bytecode. `--runtime-backend native` also requires
`--obf-code 2` and rejects levels 0 and 1.

For a native runtime, `--runtime-crypto` is compatibility metadata and does not
select a Python crypto implementation. The native runtime uses compiled platform
cryptography and has no generated-application dependency on `cryptography` or
`_pure_crypto.py`.

The reusable asset and permit service above illustrate the contract. This
repository has not yet published a production Authenticode- or
Developer-ID-signed asset or production permit endpoint. Publisher provisioning,
signing, rotation, and service requirements are documented in
[Reusable native runtime architecture](docs/RUNTIME_PACKS.md).

#### Publisher-only Windows common-runtime build

This command is for the PyEncode runtime publisher, not for a customer protecting
an application. Run it on Windows x64 with standard-GIL CPython 3.14 and a pinned
native-toolchain descriptor. Authenticode remains the default, fail-closed policy;
it additionally requires Windows SDK SignTool, a trusted Code Signing certificate,
and the certificate provider's RFC 3161 timestamp URL.

List usable certificates and copy the intended certificate's thumbprint exactly:

```powershell
Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert |
  Format-Table Subject, Thumbprint, NotAfter, HasPrivateKey
```

Then run the production-only wrapper, replacing the certificate and timestamp
placeholders with values supplied for that signing identity:

```powershell
& .\tools\build_common_runtime_windows.ps1 `
  -Python "C:\Python314\python.exe" `
  -Toolchain ".\build\native-toolchain.json" `
  -SigningThumbprint "<40-HEX-CERTIFICATE-THUMBPRINT>" `
  -TimestampUrl "<RFC3161-TIMESTAMP-URL>"
```

Use `-CertificateStore local-machine` when the certificate is in
`LocalMachine\My`, and `-SignTool` if Windows SDK SignTool is not discoverable.
The wrapper installs `cryptography` into an isolated temporary directory and
removes it afterward; pass `-NoBootstrap` only when that dependency is already
available to the selected CPython 3.14 interpreter.
The SHA-1 thumbprint selects the certificate; the binary and RFC 3161 timestamp
digests are still SHA-256.

To deliberately publish the common Windows runtime without Authenticode, use the
explicit unsigned policy and omit every signing option:

```powershell
& .\tools\build_common_runtime_windows.ps1 `
  -Python "C:\Python314\python.exe" `
  -Toolchain ".\build\native-toolchain.json" `
  -Unsigned
```

`-Unsigned` is not a fallback: the build fails if the freshly compiled PE does not
report the exact Windows Authenticode status `NotSigned`. It never discovers or
invokes SignTool and never requests a timestamp. Supplying `-SigningThumbprint`,
`-TimestampUrl`, `-CertificateStore`, or `-SignTool` together with `-Unsigned` is
an error. Here “unsigned” applies only to Windows Authenticode; the immutable
runtime catalog is still signed by the separate vendor Ed25519 key.

Both policies compile and inspect the PE, verify the applicable Authenticode state,
then re-inspect and import the final `.pyd`. Only after those checks does the tool
calculate the final SHA-256 and create the vendor-signed `runtime-asset.json` and
release ZIP. Never sign, patch, strip, or re-sign the `.pyd` afterward because any
byte change invalidates the catalog.

The public return defaults to
`build/pyencode-common-cp314-win-amd64-v1-return/`; its sibling
`*-publisher-private/` directory is a publisher key vault and must never be copied
to Git, PyPI, an application, or a customer. Use `-KeyPasswordFile` to encrypt its
PKCS#8 files in addition to the restricted NTFS ACL. Customers reuse the immutable
catalog-bound runtime and do not need SignTool, a certificate, Windows SDK, Cython,
or Zig.

#### Publisher-only macOS common-runtime build

This command is for the PyEncode runtime publisher, not for a customer protecting
an application. Run it from the prepared repository on an Apple Silicon Mac with
Xcode Command Line Tools and a usable **Developer ID Application** identity in the
login keychain:

```bash
bash tools/build_common_runtime_macos.sh
```

The script first tries `/opt/homebrew/bin/python3`. If that default is missing or
is not standard-GIL CPython 3.14 for arm64, it downloads a pinned CPython 3.14.7
Apple-Silicon `python-build-standalone` archive, verifies its fixed SHA-256, and
uses it only inside the isolated temporary build directory. The downloaded
interpreter, archive, virtual environment, Cython, and `cryptography` are removed
on success or failure. Pass `--no-python-bootstrap` to prohibit that download.
An explicitly selected `--python` or `PYENCODE_BUILD_PYTHON` remains fail-closed
and is never silently replaced.

The script detects an unambiguous Developer ID Application identity, builds the
exact `_runtime.cpython-314-darwin.so`, signs it with a secure timestamp and
hardened-runtime options, and fails closed if the Mach-O target, export,
dependencies, signature, or post-signing digest is wrong. It never accepts an
ad-hoc identity for a release runtime.

Use explicit values when the Mac has several signing identities, Python is
elsewhere, the family is being rotated, or notarization credentials are configured:

```bash
bash tools/build_common_runtime_macos.sh \
  --python /opt/homebrew/bin/python3 \
  --identity "Developer ID Application: Publisher Name (TEAMID)" \
  --runtime-id pyencode-common-cp314-macosx-arm64-v1 \
  --output build/pyencode-common-cp314-macosx-arm64-v1-return \
  --notary-profile pyencode-notary
```

`--notary-profile` is optional. When supplied, the script submits the exact signed
runtime archive with `xcrun notarytool`, waits for `Accepted`, and records the
submission result and log. Omitting it produces a Developer-ID-signed but
**not notarized** candidate; that distinction must remain visible in release notes
and must not be represented as a notarized production asset.

The script generates fresh RSA unwrap, Ed25519 permit-issuer, and Ed25519 vendor
keys for the new family. It writes the complete public release handoff to the
selected `--output` directory and writes the private keys to a different,
create-only publisher vault. The defaults are:

```text
build/pyencode-common-cp314-macosx-arm64-v1-return/
|-- runtime-release/
|   |-- _runtime.cpython-314-darwin.so
|   `-- runtime-asset.json
|-- pyencode-common-cp314-macosx-arm64-v1-runtime-release.zip
|-- SHA256SUMS.txt
|-- public/
|   |-- wrap-public.der
|   |-- issuer-public.raw
|   `-- vendor-public.raw
|-- evidence/
|   |-- runtime-handoff.json
|   |-- codesign.txt
|   |-- lipo.txt
|   |-- nm.txt
|   |-- otool.txt
|   |-- otool-headers.txt
|   |-- otool-load-commands.txt
|   |-- import-smoke.txt
|   |-- notary-submission.json          # only with --notary-profile
|   |-- notary-log.json                 # only with --notary-profile
|   `-- notary-requirement.txt          # only with --notary-profile
|-- pyencode-common-cp314-macosx-arm64-v1-notary-upload.zip  # optional
`-- COPY-BACK.txt

build/pyencode-common-cp314-macosx-arm64-v1-publisher-private/
|-- unwrap-private.pem
|-- issuer-private.pem
|-- vendor-private.pem
|-- issuer-public.raw
|-- vendor-public.raw
|-- wrap-public.der
|-- family-private.json
`-- DO-NOT-SHIP.txt
```

`runtime-release/` and its `*-runtime-release.zip` are already the complete,
canonical, vendor-signed runtime asset; Windows must verify and consume them, not
regenerate `runtime-asset.json`. `public/` contains the exact public material for
cross-checking the catalog, but a trusted pinned copy of the vendor key remains the
trust root; proximity to `runtime-asset.json` is not authentication. `evidence/`
records the signed binary and build checks. When notarization is not requested,
`runtime-handoff.json` records `not-submitted` and no notary-only files or upload
ZIP are produced.

Copy only the `*-return` directory through the ordinary handoff and verify
`SHA256SUMS.txt` after transfer. Never combine it with the sibling
`*-publisher-private` vault. The vault is created with restrictive permissions,
but its PKCS#8 keys are password-encrypted only when `--key-password-file` is
supplied. Back it up or provision its issuer/vendor credentials through a separate
publisher-controlled secret channel; never place it in Git, a protected app, a
customer ZIP, or the public return directory.

Do not run `stapler` against the bare `.so` or its ZIP. Apple can notarize an
archive and issue an online ticket for the nested standalone binary, but does not
support stapling a ticket directly to either item. A distributable outer app,
bundle, disk image, or installer should be notarized and stapled by its publisher
when that format supports stapling.

A valid Developer ID signature and an accepted notarization are necessary release
checks, not a promise that every embedding host will load the extension. Fusion's
hardened-runtime library-validation policy is independent and can reject a
third-party Team ID. Validate the exact runtime in the released Fusion version from
a quarantined, freshly extracted add-in, then close and reopen Fusion before
publishing the family.

At level 2, a load such as
`obj.member`, a store such as `obj.member = value`, and deletion or augmented
assignment of that attribute retain normal descriptor and evaluation-order
semantics, while `member` is absent from the transformed executable operation.
Method calls and longer chains are transformed the same way. Public function,
class, method, and callback names are not renamed.

Application AOT is independent of the reusable runtime. `--native-module` may still
compile selected application implementation modules through a pinned customer
toolchain; it neither changes the common runtime nor gives the customer its unwrap
or permit-signing keys. This advanced feature is not required for ordinary licensed
native-runtime builds.

Sign and key-bind additional dependencies or support trees:

```bash
pyencode build src -o dist/app \
  --entry myapp.main \
  --support build/vendor _vendor \
  --support public/config.json config.json
```

`--support SOURCE DEST` may be repeated. If `SOURCE` is a directory, its tree is
copied below `DEST`; if it is a file, `DEST` is the output file path. Symlinks,
reparse points, paths that escape the root, and case-insensitive path collisions are
rejected.

The exact destination `_vendor` is treated as a bootstrap dependency path for hosts
that do not already have `cryptography` installed, such as Fusion. Only the exact
`_vendor` entry remains on `sys.path` while the verifier loads; every other bundle
subdirectory is isolated. Because the vendored cryptography library must run before
Ed25519 can verify the bundle itself, `_vendor` is a trusted bootstrap component by
design in the default `cryptography` runtime. Vendor only wheels obtained from trusted
sources.

For an embedded host that disallows the native extension in `cryptography`, do not
vendor it. Build instead with the wire-compatible dependency-free backend:

```bash
pyencode build src -o dist/app --entry myapp.main \
  --runtime-crypto pure-python
```

This writes an empty generated `requirements.txt`. Both cryptographic
implementations consume the same signed manifest and PYE2 AES-256-GCM artifacts,
so the choice does not change the authentication algorithms or container format.
Pure-Python AES and big-integer Ed25519 operations are not constant-time and decrypt
more slowly.

For Fusion on macOS, use this portable combination when no compatible,
publisher-signed `cp314-macosx_arm64` runtime asset has been released:

```bash
pyencode build src -o dist/app --entry myapp.main \
  --obf-code 1 \
  --runtime-backend python \
  --runtime-crypto pure-python
```

Main options:

- `-o`, `--output`: output directory; it must not exist or must be completely empty.
- `-e`, `--entry`: dotted name of the entry module or package.
- `--exclude GLOB`: exclude paths matching a glob; may be repeated.
- `--no-resources`: do not copy non-Python files from the source tree.
- `--keep-docstrings`: preserve docstrings.
- `--optimize {0,1,2}`: CPython optimization level.
- `--expires YYYY-MM-DD`: refuse to run after the specified UTC date.
- `--launcher PATH`: use a custom `.py` launcher.
- `--support SOURCE DEST`: copy, sign, and key-bind a support file or tree.
- `--rename-locals`: rename metadata for local variables that are not parameters.
- `--obf-code {0,1,2}`: choose module-only decryption (`0`, the default), add
  authenticated per-function capsules and dispatch stubs (`1`), or additionally
  encrypt eligible statically spelled attribute names and dispatch their load/store/delete
  operations through the runtime protector (`2`). The reusable native backend
  always requires level `2`.
- `--allow-extra-data`: allow the host to create additional unsigned data files and
  directories; signed files and recognized Python/native artifact types remain
  protected. The application must not execute or trust the added data.
- `--unsigned-data PATTERN`: copy matching discovered source resources only after
  the signed/key-bound inventory is sealed; repeat for additional globs. This
  requires `--allow-extra-data`.
- `--runtime-crypto {cryptography,pure-python}`: select the portable Python
  runtime's cryptographic implementation. The default is `cryptography`; the
  native runtime always uses its compiled platform boundary.
- `--runtime-backend {python,bytecode,native}`: emit the portable Python runtime
  (the default), five sourceless CPython-minor-bound `.pyc` runtime files, or one
  publisher-signed reusable runtime. Native mode has no portable or locally
  compiled fallback.
- `--runtime-asset DIR`: select the directory containing the exact runtime binary
  and canonical signed `runtime-asset.json`.
- `--runtime-vendor-key FILE`: pin the Ed25519 vendor public key used to verify the
  runtime asset metadata.
- `--runtime-id ID`: require an exact immutable runtime-family version.
- `--permit-url HTTPS_URL`: request authorization for the exact build from the
  license service; it defaults to `PYENCODE_PERMIT_URL`.
- `--license-key KEY`: supply a PyEncode builder license. Prefer the
  `PYENCODE_LICENSE_KEY` environment variable so it is not recorded in shell
  history.
- `--permit-timeout SECONDS`: bound the permit-service request time.
- `--native-toolchain FILE`: use a canonical, hash-pinned Windows toolchain only
  for the independent `--native-module` application-AOT feature. It is not used to
  build or alter the reusable runtime.
- `--native-module MODULE`: compile this exact dotted implementation module as a
  native extension; repeat for additional modules. Package `__init__`, entry,
  `__main__`, top-level, duplicate, missing, globbed, and non-ASCII names fail.

`--rename-locals` is opt-in because code that uses a dynamic alias of `locals()`,
reads `frame.f_locals`, or depends on debugger/framework introspection may require
the original local names. Arguments, closures, line tables, and exception tables are
always preserved.

`--obf-code`, `--optimize`, `--rename-locals`, and `--keep-docstrings` govern
encrypted `.pye` modules. Native-selected application modules use the fixed
directives recorded in
`.pyencode-native.json` (`binding=False`, no embedded signatures/code comments,
no tracing/profile hooks, and docstrings disabled) so coverage is auditable.

## Custom launchers and embedded hosts

A custom launcher is copied byte-for-byte, signed, and included in key derivation.
It must set:

```python
import sys
sys.dont_write_bytecode = True
```

before any non-bootstrap import. The builder enforces this minimum requirement. A
secure production launcher must also verify that `pyencode_runtime` contains no
`__pycache__`, `.pyc`, or unexpected files **before** importing the package. The
allowed set must name the selected backend exactly: `_runtime.py` for the Python
backend, or the target-specific reusable extension plus `_seal.json` for a common
native family. The default launcher and the Weldments pipeline include this
complete preflight check.

A host such as Fusion can load an entry point without placing its plaintext module
name in the stub:

```python
import sys
sys.dont_write_bytecode = True

# Perform the pyencode_runtime preflight here.
from pyencode_runtime import load_entry

implementation = load_entry()
```

`install()` intentionally returns `None`. `load_entry()` returns the entry module to
an embedding host, while `run()` executes the entry point with `__main__` semantics.

## Output layout and runtime behavior

The native layout below shows both target alternatives for reference; one build
contains exactly one of the two reusable runtime extensions, never both.

```text
dist/app/
├── .pyencode-manifest.json
├── .pyencode-native.json               # only with application AOT
├── .pyencode-runtime-native.json       # reusable runtime_id/asset report
├── myapp/
│   ├── 14a0...f91c.pye
│   ├── 7b82...bc33.pye
│   ├── core.cp314-win_amd64.pyd        # only if selected by --native-module
│   └── assets/config.json
├── pyencode_runtime/
│   ├── __init__.py
│   ├── _build.py
│   ├── _mp_main.py
│   ├── _pure_crypto.py                 # Python backend only
│   ├── _runtime.py                     # Python backend only
│   ├── _runtime.cp314-win_amd64.pyd    # Windows reusable family
│   ├── _runtime.cpython-314-darwin.so  # macOS arm64 target alternative
│   └── _seal.json                      # signed permit + wrapped app key
├── requirements.txt
└── run.py
```

The native runtime report records the exact `runtime_id`, target, and signed asset
hash. The copied runtime bytes are identical to the publisher asset. This report is
distinct from `.pyencode-native.json`, which records only application modules
compiled through `--native-module`. Both reports are signed when present. For a
common native runtime, `_seal.json` is the one deliberate exception to the
manifest `integrity.files` set: its own canonical Ed25519 permit signature and the
permit's `application_commitment_sha256` provide the non-circular binding.

Resources are not encrypted, but their build-time bytes and paths are signed and
key-bound. By default, the entire output tree is immutable: the runtime rejects any
file or directory that is added, removed, or modified.

For hosts that create metadata or caches beside an add-in, `--allow-extra-data`
permits additional data files and directories without invalidating the bundle. This
mode still rejects modifications or removal of signed files, unexpected `.py`,
`.pyc`, native libraries, or `.pye` files, and all symlinks or reparse points. Do not
use this extra data as trusted input for licensing or security decisions. The
Weldments pipeline enables this compatibility mode so Fusion can create `.vscode`,
logs, or machine-specific caches.

Files that the application itself updates, such as user settings, host metadata, or
profile libraries, must not be part of the signed source inventory. Use repeatable
`--unsigned-data` patterns to copy their initial values safely without a separate
post-build step:

```bash
pyencode build src -o dist/app --entry myapp.main \
  --allow-extra-data \
  --unsigned-data '*.png' \
  --unsigned-data 'myapp/data/settings.json'
```

Patterns are matched case-sensitively against the POSIX path relative to the output
root and, for convenience, against the basename. Thus `*.png` selects PNG resources
at any depth; `**/*.png` is also accepted and includes root-level matches. Every
pattern must match at least one resource discovered in the main source tree.
`--unsigned-data` does not apply to `--support` inputs, and `--no-resources` leaves
nothing eligible for selection.

Selected files are copied into the atomic temporary output only after the signed
manifest and key-bound inventory have been sealed. Their paths and bytes are absent
from `integrity.files`, are not inputs to key derivation, and may be modified or
removed by the host. The signed manifest records only the existing
`allow_extra_data=true` compatibility policy; it deliberately does not present the
patterns or selected files as trusted inputs.

The builder rejects a pattern if it selects Python source/bytecode, `.pye`, native
libraries, executables or executable scripts, symlinks/reparse points, non-regular
entries, the launcher, `pyencode_runtime`, either generated manifest/report, or
`requirements.txt`. Added unsigned files are still scanned at runtime: recognized
code/native artifacts and every link/reparse point remain forbidden. Never execute
or use unsigned data for licensing, integrity, or other security decisions.

The `__file__` value of a protected module is the real path to its randomly named
artifact. Consequently, `Path(__file__).parent`, `pkgutil.get_data()`, and
`importlib.resources`, including nested directory resources, continue to work. Code
that depends on the basename or stem of `__file__` sees the random token.

Integration tests cover imports by full name, relative imports, circular imports,
namespace packages, Unicode module names, reloads, `runpy.run_module()`, and
`multiprocessing` spawn for applications launched through the default launcher.
`pkgutil.iter_modules()` and `walk_packages()` cannot automatically discover
protected child names because those names intentionally reside in the encrypted
index; importing a known full name still works.

For an embedding host that calls `load_entry()`, spawning works if the child process
also runs the host bootstrap. A host that places its bootstrap only inside
`if __name__ == '__main__'` and then directly spawns a protected target requires a
dedicated launcher. Automatic spawn bootstrapping currently focuses on applications
executed through `run()`.

Versions before 1.0 use one fixed runtime namespace per process. Do not load two independent
PyEncode distributions into the same interpreter; run them in separate processes.
Randomized or multi-bundle runtime namespaces are a compatibility item to resolve
before declaring a stable 1.0 API.

## Security limitations

PyEncode makes static analysis harder, but it cannot guarantee secrecy on a machine
fully controlled by an attacker:

- Portable execution is offline, so key material must remain recoverable from the
  bundle. An experienced analyst can inspect the Python runtime and reproduce key
  derivation. The reusable native runtime instead embeds one common-family private
  unwrap key in compiled code. Each build has a unique application master key and
  stores only its RSA-OAEP-wrapped form in the signed permit. Binary analysis,
  native memory inspection, or a debugger can still recover these values.
- The common runtime is intentionally shared. If an attacker extracts its private
  unwrap key, every protected application using that `runtime_id` is inside the
  same compromise domain. Unique application master keys prevent direct key reuse
  but do not contain this family-wide blast radius. Recovery requires publishing a
  new immutable family and rebuilding applications; rotation cannot retroactively
  protect offline outputs made with the old family.
- Code objects must exist in memory while they execute. Tracing, debuggers,
  memory inspection, code-object watchers, or native hooks may still observe code
  and runtime state. The native codec avoids the ordinary `marshal`, `code.__new__`,
  and `function.__new__` audit paths, but this does not make it debugger- or
  `PyCode_AddWatcher`-proof.
- At `--obf-code 1`, the original code for protected top-level functions is not
  present in the module code object at rest, and normal `function.__code__`
  inspection sees a generic stub. The real `CodeType` is nevertheless materialized
  for execution. A suspended generator/coroutine retains an executing frame, and an
  attacker who controls the process can inspect or intercept it. This option raises
  extraction cost; it is not a confidentiality boundary.
- At `--obf-code 2`, ordinary source-level `obj.attribute` operations carry an
  encrypted attribute-name capsule instead of placing that spelling in the
  executable operation. The runtime must nevertheless decrypt and materialize the
  real name to use Python's object model. The portable runtime caches validated names
  as Python strings. The Windows native runtime instead caches UTF-8 names in an opaque
  C hash table, exposes only a `PyCapsule` to Python GC, and zeroes each allocation when
  released; a transient `str` still exists for every actual attribute operation. An
  attacker controlling the interpreter can inspect execution or invoke a captured
  capsule against a proxy object which reports the requested name. Explicit strings
  passed to `getattr`/`setattr`, annotation metadata,
  dotted value/class patterns, `__slots__`, string literals, and other reflective data
  are intentionally not rewritten. The conservative local/frame-introspection guard
  attributes used by `--rename-locals` also remain visible so that enabling both
  options does not defeat the guard. Level 2 replaces CPython's specialized
  `LOAD_ATTR`/`LOAD_METHOD` path with runtime dispatch, so attribute-heavy code may be
  materially slower and should be benchmarked against the real workload. On CPython
  3.13 or newer it can also change introspection-only `type.__static_attributes__`
  metadata because the compiler sees a protected subscription rather than
  `STORE_ATTR`; code relying on that metadata should remain at level 1.
- The integrity interlock prevents straightforward file modification, artifact
  substitution, code injection, and re-signing. It does not turn a pure-Python
  runtime into a native trust anchor. A native runtime is a stronger obfuscation
  boundary, not a hardware-backed trust anchor.
- Module and entry-point names are hidden at rest, but appear during import in
  `sys.modules`, tracebacks, and runtime state. Package and resource directories may
  still reveal part of the application structure.
- `--expires` relies on the system clock and is not a replacement for a licensing
  system.
- Native-selected modules no longer expose a marshal-loadable application code
  object through the PyEncode runtime, but Cython binaries still reveal useful
  names, strings, metadata, and machine code to a skilled reverse engineer.
- Native runtime and application extensions remain patchable in a process fully
  controlled by an attacker.
  The signed finder raises the cost of preload, path-shadow, and file-tamper attacks;
  it is not an unpatchable hardware trust anchor or a guarantee of equivalence to
  any PyArmor protection level or configuration. Matching the documented attribute-
  chain semantics of its level 2 does not reproduce PyArmor's proprietary runtime or
  separate RFT/BCC/private/restrict features. The products use different
  runtimes and should be compared with repeatable attacks against the exact builds,
  not by matching option names.
- The native runtime is loaded before it can verify the application inventory.
  Its vendor-signed metadata and exact hash detect accidental corruption and
  inconsistent replacement, but are not hardware trust anchors. The publisher
  must finish the declared Windows signing policy (Authenticode or verified
  `NotSigned`) or sign the macOS `.so` before computing and signing
  `runtime-asset.json`; customers then copy those bytes unchanged. This
  repository does not yet claim that a production Authenticode- or
  Developer-ID-signed asset exists. A notarized macOS runtime also remains subject
  to the embedding host's independent library-validation policy.
- The native `CodeType` record uses CPython's C pickle implementation only after
  the signed artifact hash and AES-GCM tag have been verified. Its schema checks
  are for trusted build output; it is not a general-purpose decoder for untrusted
  pickle data.
- Native files are re-hashed immediately before CPython loads them, but the hash and
  operating-system loader open are not one atomic operation. An attacker that already
  controls the process or filesystem may still exploit that narrow replacement window.

Never store a permit-signing key, publisher signing credential, or plaintext
application master key in the Python CLI. The licensed flow sends only a canonical
permit request and license credential to the service; it does not send source or
the plaintext master key. A reusable native runtime and `--obf-code 2` primarily
add reverse-engineering cost. End-user application licensing, online revocation,
or device binding remains a separate design.

## Testing

```bash
python -m unittest discover -v
```

CI runs on Windows and Ubuntu with CPython 3.10 through 3.15, plus macOS
CPython 3.14 compatibility jobs. Portable and bytecode tests cover PYE2, the
encrypted module index, KDF vectors, tampering and re-signing, resource/support
integrity, opaque filenames, imports, packages, namespaces, Unicode, `runpy`,
`multiprocessing` spawn, ordinary launchers without `-B`, expiration policy, and
atomic output creation. Level-1 tests cover build-time function capsules, ordinary
functions and methods, lambdas, closures, generators, coroutines, call behavior,
and the idle dispatch-stub invariant. Level-2 tests additionally cover encrypted
attribute loads, method calls, chains, stores, deletion, augmented and unpacking
assignment, descriptor behavior, private-name mangling, tamper rejection, and the
annotation, pattern-matching, and local-reflection exclusions.

The reusable-runtime suite covers strict canonical asset metadata, vendor
signatures, exact target filename/hash/size checks, symlink and path
rejection, byte-identical copying, per-application RSA key wrapping, signed build
permits, target and `runtime_id` binding, the mandatory full-manifest application
commitment, the independently authenticated seal outside `integrity.files`, HTTPS
permit-client behavior, and tamper rejection. Publisher-only Windows jobs may also
compile and inspect test binaries; they do not constitute or publish a production
Authenticode-signed runtime asset. Application-AOT tests remain independent of
this runtime-family contract.

Production macOS runtime jobs must build, Developer ID sign, optionally submit for
notarization, inspect, import, and run the exact immutable family before its
metadata is published. A production family advertised as notarized must have an
`Accepted` notary result. That publisher pipeline is not a customer build
requirement, and Fusion compatibility still requires a quarantined host-level
load test.

## Releasing

The production release process using GitHub OIDC is documented in
[RELEASING.md](RELEASING.md). A tag alone never publishes source. The workflow
uploads only the two audited protected wheels attached to a published GitHub
Release whose tag exactly matches the project version, for example `v0.7.0`.
