# Local plugin protocol v1 Plugins are trusted local programs, not sandboxed extensions. They receive ordinary filesystem paths and inherit the host environment. The host owns catalog and archive writes. Do not launch imported game programs from the supplied adapters. The configured command receives one additional argument: an absolute request JSON filename. Its working directory is a disposable workspace. Input/output bytes are never embedded in JSON. ```json { "protocol": 1, "input_dir": "/workspace/input", "output_dir": "/workspace/output", "result_file": "/workspace/result.json", "settings": {}, "workspace_bytes": 107374182400 } ``` Write retained output files under `output_dir` and finish with a result file: ```json { "protocol": 1, "layer": "extracted", "coverage": "partial", "warnings": ["Nested custom archive retained as opaque content"], "files": ["game/main.elf", "assets/opaque.pack"] } ``` `layer` is `extracted` or `derived`. Coverage is `complete`, `partial`, or `unknown`. Every regular output file must occur exactly once in `files`. Paths must be relative and free of traversal. Live output symlinks are rejected; represent them as metadata until a portable output-entry schema is added. An unknown field or unsupported protocol fails validation. A successful exit is necessary but insufficient. The host validates the inventory, captures output bytes, archives them, and verifies read-back hashes before publishing an immutable child snapshot. Parent inputs remain retained even when coverage is declared complete. `coverage` describes the plugin operation, not permission to discard inputs. The host records configured tool version, settings, input artifact identities, stage, output snapshot, and failure state. Version/build identities must be updated when plugin behavior changes. Automatic cache reuse and full dependency-build hashing are not yet implemented. A rerun always creates a new record; identical bytes still deduplicate. **Settings are durable public provenance, not a secret store.** Put decryption keys in environment variables or private files referenced by non-secret identifiers in settings. No environment values are copied into manifests. The bundled SPIKE wrapper does not introduce new decryption support beyond the configured upstream tool. Plugin stdout/stderr are discarded by the host. On failure, a plugin may write `{"protocol":1,"error":"brief diagnostic"}` to its result file before exiting nonzero; the host records a bounded diagnostic. Successful Ghidra runs retain analysis/reopen logs. Plugins must keep secret values out of all diagnostics and retained outputs. Input materialization uses less than half the configured workspace limit, reserving capacity for output and capture. The host polls workspace size and kills the process group on timeout/budget violation. CPU/RAM quotas, cgroups, automatic tmpfs placement, resumable steps, and shared worker scheduling are not implemented. Do not use this polling limit as a hard disk quota. ## ZIP The example config registers `plugins/zip_extract.py`; Python's standard library is sufficient. It handles ZIP containers from any publisher, and reports non-ZIP supporting inputs as opaque in the retained parent. Paths are prefixed with `.entries/` to avoid cross-package collisions. Nested extraction can be requested by running another plugin on the output snapshot. ## SPIKE Build/install the [upstream spike-spk tool](https://github.com/bdash/spike-spk) separately and calculate its executable SHA-256. Its source supports `verify PATH` and `extract PATH --output DIR`, including first split-package parts; the wrapper delegates structure validation to it. The framework does not claim support for all three SPIKE generations. Add a plugin config (replace paths and digest): ```json "spike-extract": { "command": ["python3", "/absolute/verstack/plugins/spike_extract.py"], "version": "spike-adapter/2+PINNED-UPSTREAM-COMMIT", "settings": { "tool": "/opt/spike-spk/bin/spike-spk", "expected_sha256": "SHA256-OF-LOCAL-EXECUTABLE" }, "timeout_seconds": 7200 } ``` Import all split parts together as a directory. ZIP-wrapped packages can first use `zip-extract`, then `spike-extract`. Supporting files stay in parent snapshots. Symlinks emitted by the upstream tool are recorded in `package-evidence.json` rather than followed. This adapter has passed the [Game of Thrones pilot](local-pilot.md); other wrappers and titles still need testing. Upstream extraction validates checksums. `max_threads` defaults to 2; the upstream split reader may materialize a large inner package in RAM. ## Ghidra Run `python3 scripts/install_local_tools.py` from the project root to install the pinned official Ghidra and JDK archives under `tools/`, with SHA-256 verification from `docs/toolchain.lock.json`. Downloading requires network access; normal analysis is local. The script is intended for Linux x86-64 and Python 3.11 or newer. Launchers are `tools/bin/ghidra` and `tools/bin/ghidra-headless`. Alternatively, configure an existing local distribution. The adapter uses [headless analysis](https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/RuntimeScripts/support/analyzeHeadlessREADME.md) and the documented [GZF exporter](https://ghidra.re/ghidra_docs/api/ghidra/app/util/exporter/GzfExporter.html). A `.gzf` represents one analyzed program, not an entire multi-program project. ```json "ghidra": { "command": ["python3", "/absolute/verstack/plugins/ghidra/analyze.py"], "version": "ghidra-adapter/2+LOCAL-GHIDRA-VERSION", "settings": { "ghidra_home": "/opt/ghidra", "java_home": "/opt/jdk-21", "max_heap": "2G", "expected_version": "EXACT-application.version-FROM-application.properties", "max_cpu": 2, "analysis_timeout_seconds": 3600 }, "timeout_seconds": 14400 } ``` An optional `paths` array selects logical executable paths. Otherwise ELF/PE magic selects candidates and Ghidra chooses the loader/processor. Generation is not used to guess architecture. Each unique selected binary produces `/program.gzf`, `functions.json`, `reopened.json`, `analysis.log`, and `reopen.log`. Download the `.gzf` through the UI for local review. Java user/cache/temp directories stay in the disposable workspace. External or uninitialized function bodies receive no exact-body hash and cannot establish an exact match. The adapter fails on analysis timeout, input identity mismatch, exporter failure, or absence of the reopen receipt. It retains baseline outputs only; manual GZF reimport, enrichment, and automatic symbol propagation are future operations. ## LUKS credential validation `plugins/luks_check.py` checks LUKS2 headers in files and ZIP-wrapped split packages through local `cryptsetup open --test-passphrase`. It does not mount filesystems or extract decrypted content. Configure exactly one `key_file` or `key_env` reference in settings, plus optional `cryptsetup` executable path. `key_encoding` defaults to `raw`; `auto` tests raw bytes, stripped text, and (for 0x-prefixed 32-bit word lists) little-endian bytes, big-endian bytes, and concatenated hexadecimal text. Credentials are passed through stdin, never argv, logs, manifests, or temporary keyfiles. The retained `luks-key-check.json` records only the reference, header UUID, attempted encoding names, fixed status values, and `unlock_verified`. A completed diagnostic run can report failed unlocking; it does not imply successful extraction. `tool_error` is distinct from a rejected credential. The header workspace is limited to the first 16 MiB per candidate; layouts requiring more keyslot data need an extended adapter. `key_encoding: "vcmailbox"` accepts a complete 16-word response to the SPIKE 3 boot script's mailbox request (tag `0x00030021`, offset 0, count 8). It validates the response envelope and converts only the eight key words, in display order, to 32 bytes, matching the boot script's `xxd` conversion. `auto` includes this format when the envelope matches. Packing the entire response as a key includes protocol words and fails unlocking. For `import-extract`, configure `disk_key_file` and `disk_key_encoding` separately from `key_file`. These settings apply to encrypted SD-card partitions; update packages continue using the update credential. For example: ```json "disk_key_file": "/absolute/sd-mailbox-response.txt", "disk_key_encoding": "vcmailbox" ``` Validated using a generated LUKS2 fixture with correct/incorrect keys and the supplied Pokémon LE/Pro headers. This machine has cryptsetup 2.8.4. No additional system packages were needed. ## SPIKE 3 encrypted update extraction `plugins/luks_extract.py` accepts one `input_path` within a snapshot: a ZIP containing a complete, consistently numbered split SPK set, or an assembled LUKS2 file. Configure exactly one raw `key_file` or `key_env` reference. It assembles parts in numeric order, obtains the validated volume key in memory through libcryptsetup, decrypts AES-XTS sectors into a disposable filesystem image, and uses read-only `debugfs` to recover the inner SPK. There are no kernel mounts, device-mapper changes, or keyfiles written to the workspace. The first implementation supports a single LUKS2 AES-XTS-plain64 segment with 512- or 4096-byte sectors and an ext4 filesystem containing an SPK. Unsupported layouts fail explicitly. It checks a conservative four-times-container workspace allowance before assembly; the host continues polling total usage. Original inputs remain retained and filesystem metadata is not promised byte-identical restoration. XTS is not authenticated encryption: the following SPK stage must still verify its payload checksums/HMACs. Local dependencies used in the pilot: libcryptsetup/cryptsetup 2.8.4, Python cryptography 46.0.5, and debugfs/e2fsprogs 1.47.2. These are optional plugin dependencies; generic core operations do not require them. The installed local configuration records these dependency versions and selects Pokémon LE. `input_path` must be changed (or another plugin configuration registered) for another package. The standard SPIKE parser rejected observed system package type 4. A minimal local patch, `patches/spike-spk-type4.patch`, adds that numeric variant without disabling MD5 or HMAC verification. It is built as a separate executable and plugin registration so the existing pinned SPIKE 1/2 tool remains available. ## Godot script recovery Install the pinned official Linux decompiler bundle with `python3 scripts/install_godot_tools.py`, then regenerate local pipeline settings with `scripts/configure_pipeline.py`. The `godot-scripts` adapter verifies every pinned tool file before using GDRE's headless recovery and tokenization commands. Game scripts are never launched. User data and logs remain in disposable scratch. The adapter enumerates scripts in supported unencrypted PCK v1/v2 packages, checks their package MD5s, detects the recorded engine version, recovers every script, and checks that compiling and decompiling the recovered source preserves its text. Output scripts keep package-relative paths. This validates source recovery, not the runtime behavior of the complete game. Plain `.gd` inputs are retained as source; loose `.gdc` inputs require an explicit `bytecode_version` setting or their original package. Encrypted/custom/unsupported inputs produce visible coverage failures. Recovered scripts have an 8 MiB per-file bound and share the configured output budget. `godot-scripts-evidence.json` records source and script hashes, package entry and offset, engine version, round-trip status, and literal resource references. The optional stage runs before media extraction during asset imports; retrying older imports adds it while reusing existing output snapshots. The workbench's Scripts tab exposes recovered source, folders, filtering, downloads, and the recovery report. See [asset correlation findings](asset-correlation.md).