# Frozen .etplugin package contract v1

M0 froze naming, metadata, encoding, limits and test vectors. M5 implements
native archive verification, per-user signed installation, rollback and
entitlement enforcement. A structurally valid manifest or test signature does
not authorize loading; a trusted publisher public key is required.

`.etplugin` is a ZIP archive with UTF-8 names, stored/deflated regular files only.
Reject encryption, symlinks/hardlinks/special files, duplicate or case-colliding
paths, directory traversal, absolute paths, backslashes, drive/stream separators,
Windows device names and trailing spaces/dots. Directory entries are optional and
carry no payload; directories are otherwise implicit. ZIP64 is unnecessary and
rejected in v1. Limits: 128 MiB archive, 256 MiB total expanded, 64 MiB per file,
4096 payload files, 240 ASCII bytes per portable path, maximum 8 components, and
100:1 maximum per-file expansion ratio. Enforce bounds while streaming, not after
unbounded allocation or extraction. Manifest/signature JSON are each <=64 KiB.

```text
plugin.json
signature.json
payload/windows-x64/plugin.dll
payload/linux-x64/plugin.so
payload/linux-arm64/plugin.so
assets/...
licenses/...
```

`package-manifest.schema.json` requires stable plugin/publisher/key IDs, display
name and description, strict three-component version, HTTPS help/support/home URLs,
host minimum/inclusive and maximum/exclusive range, ABI range, required/optional
feature lists, capabilities, per-target entries, panel/service declarations,
settings schema, complete file inventory and dependency/license inventory. Product/
entitlement metadata is optional and separate from package authenticity. Each
target has exactly one declared entry. Executable dependencies must be individually
inventoried under their target payload directory. Every other payload is an asset
or license; extra/unlisted payloads are rejected. Every file lists SHA-256, exact
expanded byte length, role and platform. No self-referential inventory of
`plugin.json` or `signature.json` is allowed. Dependency and panel namespaces must
agree with declarations; runtime panel registrations must agree with the manifest.

Manifest schema version and ABI/service/wire versions are independent. Unknown
manifest keys are rejected. Unknown optional features may be ignored; unknown
required features or capabilities are not granted. Native libraries match the
selected platform (including ABI architecture), not merely their filename suffix.

## Signing bytes

The detached signature uses Ed25519 as specified by [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032).
Validate the complete manifest schema before canonicalization. The JSON subset
forbids duplicate keys, invalid Unicode and floating-point numbers; numeric metadata
uses safe-range integers. Canonical bytes follow [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785),
with no whitespace, sorted object keys, unchanged Unicode string values and UTF-8
encoding. All schema object keys are ASCII, making the integer-only subset simple
to test without introducing a separate number-formatting implementation.

Sign these exact bytes (the prefix ends in one LF):

```text
UTF8("EpicTunerPluginManifest/v1\n") || canonical_manifest_bytes
```

`signature.json` has schema=1, algorithm="Ed25519", publisher key ID, SHA-256 of the
canonical manifest, and the detached 64-byte signature encoded as unpadded base64url
(86 characters). Reject noncanonical encoding. The signature binds the full manifest
and therefore every declared payload hash/length, target, capability and product.
The issuer's private key never enters the SDK, host package, logs or activation
responses. Test keys are separate from trusted publisher roots.

The native verifier uses OpenSSL Ed25519 and rechecks every payload hash before
each installed worker start. Import publisher public keys explicitly in the
Plugins window; its per-user `plugin-trust.json` has no built-in roots. A
`revoked: true` key blocks both new installs and installed version starts.
Rotate by importing a new key ID and signing an updated package; publisher ID
must remain stable across updates. Test keys in the private repository are
excluded from the SDK and normal app bundle. The runtime never treats a valid
test-vector signature as an implicit trust grant.

The publisher tool is `tools/package.py` in the exported SDK. It reads a full
v1 manifest, computes the inventory from a bounded payload tree, validates the
schema, signs the domain-separated manifest, and creates `.etplugin`. It
requires Python `cryptography` and `jsonschema` at publishing time; consumers
need neither Python nor those packages. Keep the private PEM outside the SDK,
package, source tree and support logs.

The manager stages a new version in a sibling directory, verifies it again,
renames it into an immutable version path, drains the prior worker, then
atomically saves the active-version registry. It preserves the prior version
and a separate settings snapshot for rollback. A failed new worker start
triggers rollback; manual rollback is also available. New capabilities appear
in the review dialog before the install action. Uninstall has an explicit
keep/remove settings choice. `--safe-mode` suppresses installed auto-start.
