12 KiB
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.
{
"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:
{
"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 <input-name>.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 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):
"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; 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 and the documented GZF exporter. A .gzf represents one analyzed program, not an entire multi-program project.
"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 <sha256>/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:
"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.