Safe Android Reverser exposes one public MCP control plane and executes analyzers in isolated capability workers. Capability SPI v1 is the extension contract established by 0.3.0 and extended by later milestones rather than replaced.
AI agent
|
v
safe-android-reverser MCP
host control plane
|
+-- Capability Registry
+-- Adapter Registry/factory
+-- Runtime Driver
+-- Path / Job / Evidence contracts
|
+-- isolated capability workers
The agent never chooses Docker/Podman commands and never receives raw unrestricted analyzer consoles. The host control plane is the only layer allowed to invoke the container runtime.
The 0.4 development line currently defines independently versioned contracts:
Capability API 1
Worker ABI 1
EvidenceEnvelope 1
PEG schema 2
Flutter cache schema 3
A future breaking change requires an architecture decision, migration path, compatibility tests, documentation, and senior review.
Capabilities are declared under:
plugins/safe-android-reverser/capabilities/*.json
Representative manifest:
{
"id": "framework-flutter",
"capability_api": 1,
"worker_abi": 1,
"representations": ["dart-aot", "libapp.so", "flutter-assets"],
"trust_boundary": "framework-static",
"activation": "required",
"adapter": "flutter-aot",
"protocol": "cli-json",
"image": {
"repository": "ghcr.io/salingnh/safe-android-reverser-flutter",
"role": "framework-flutter"
},
"operations": ["analyze_flutter_aot", "find_dart_symbols"],
"sandbox": {
"network": "none",
"read_only_root": true,
"drop_all_capabilities": true,
"no_new_privileges": true,
"memory": "6g",
"cpus": "2",
"pids_limit": 256,
"tmpfs_tmp": "1g",
"tmpfs_work": "512m"
}
}Unknown/missing fields are rejected. Booleans, integers, strings, arrays, IDs, operation names, image descriptors, and policy values are strictly validated.
Public operation ownership is unique across all manifests.
required
The release baseline depends on this capability. Its failure degrades overall health.
optional
Active when installed/declared by the release, but failure does not make the required platform unhealthy.
opt-in
Declared but inactive until its exact capability id is explicitly enabled through
SAFE_REVERSER_ENABLE_CAPABILITIES.
A dynamic-opt-in trust boundary must use activation=opt-in.
Disabled opt-in capabilities do not expose analyzer tools. They remain discoverable through list_capabilities as declared/not enabled.
Current identifiers:
static
framework-static
native-static
dynamic-opt-in
Static-style workers preserve:
network = none
read-only root = true
cap-drop = ALL
no-new-privileges = true
non-root UID/GID
bounded memory / CPU / PIDs / tmpfs
No worker receives a Docker/Podman socket.
Only dynamic-opt-in may declare sandbox.network=controlled.
The 0.3 Runtime Driver intentionally refuses execution of controlled. A later dynamic Runtime Driver implementation may support that already-defined policy without changing Capability SPI or public MCP topology.
These are separate concepts.
The host adapter owns capability-specific orchestration that cannot be expressed by a generic transport.
Current kinds:
mcp-container
Generic MCP-over-stdio capability worker.
flutter-aot
Flutter-specific exact runtime-cache orchestration followed by bounded CLI JSON worker calls.
Prefer existing reusable adapters. A new adapter kind must be registered behind the adapter factory/registry boundary.
It must not add framework/operation branches to generic ControlPlane.call(), health aggregation, job ownership, evidence normalization, or public MCP topology.
The worker communication protocol currently supports:
mcp-stdio
cli-json
Protocol is not framework identity. Multiple capabilities can share a protocol/adapter.
The control plane reserves:
health
list_capabilities
A capability manifest must not claim them.
Each other public semantic operation has exactly one manifest owner. Duplicate ownership causes registry startup failure.
The control plane resolves:
operation name
↓
manifest owner
↓
enabled adapter
↓
worker/domain implementation
Do not implement public dispatch as a framework/operation if/elif tree in the control plane.
Worker ABI v1 requires readiness/diagnostic behavior behind the adapter contract.
For mcp-stdio, the worker exposes an internal health tool. The generic adapter validates it, removes it from the public capability tool surface, and exposes diagnostics through the host platform health response.
CLI/domain adapters must supply equivalent bounded diagnostics.
A capability is ready only when its image/runtime identity is compatible and the worker surface satisfies the declared contract. Image existence alone is not readiness.
static-core owns generic Android package/DEX/JVM/resource triage and semantics, framework detection/routing preflight, and fast generic native triage.
It must not accumulate deep semantics for external frameworks merely because those frameworks are packaged inside APK files.
A framework capability owns semantic analysis of the representation carrying framework business logic, for example:
framework-flutter -> Dart AOT semantics
framework-hermes -> Hermes/JS semantics
framework-il2cpp -> IL2CPP metadata/native correlation
framework-dotnet -> managed assemblies/IL semantics
Generic native/JNI analysis is a substrate and escalation path. A higher-level framework analyzer remains primary when it preserves richer semantics.
Dynamic execution/device/network access is a separate explicit opt-in trust boundary and must never be introduced by weakening static workers.
The shared Runtime Driver owns:
- Docker/Podman selection;
- image inspect/pull;
- OCI provenance verification;
- immutable image-ID resolution;
- UID/GID mapping;
- network policy enforcement;
- read-only root;
- dropped Linux capabilities;
no-new-privileges;- CPU/memory/PID limits;
- bounded tmpfs;
- mount policy;
- bounded stdout/stderr capture;
- MCP stdin attachment where required.
Capability adapters must not reimplement container command construction.
A normal capability image publishes at least:
org.opencontainers.image.version
io.safe-reverser.capability.id
io.safe-reverser.capability.api
io.safe-reverser.worker.abi
The Runtime Driver:
requested image reference
↓
inspect / pull as provisioning policy
↓
verify required OCI labels
↓
resolve canonical sha256 image ID
↓
execute the immutable image ID
This closes the mutable-tag inspect/run TOCTOU gap.
Readiness exposes both requested image and verified image_id.
A long-lived control-plane instance keeps the verified immutable image identity for that worker instance; a tag mutation does not silently switch already-running code.
Generic development/test override:
SAFE_REVERSER_CAPABILITY_IMAGE_<CAPABILITY_ID>
with - normalized to _ and uppercase.
Legacy pre-0.3 image aliases may exist as configuration compatibility aliases only; image lifecycle remains Runtime Driver-owned.
Frameworks that require compiler/runtime-specific analyzers may derive a registry-independent cache identity.
Flutter binds:
cache schema
Capability API
Worker ABI
Dart version
snapshot hash
architecture
OS
compressed-pointers mode
Blutter commit
The host maps the cache tag to a configured repository, verifies all required labels, resolves an immutable image ID, and executes that ID.
The analyzer worker never clones/builds/downloads a missing Dart runtime during normal analysis.
Runtime cache resolution is a host-side semantic service:
FlutterCapability
|
v
RuntimeCacheResolver
|-- exact cache lookup and OCI-label verification
|-- private persistent state and request deduplication
|-- timeout/retry/restart reconciliation
`-- ControlledBuildProvider
|-- GitHubActionsControlledBuildProvider
`-- future provider implementations
The provider-neutral state model is READY, BUILD_REQUIRED, BUILDING, and FAILED. The stable provider request identity is the SHA-256 of canonical JSON containing every exact runtime-identity field. It identifies the desired cache and does not change across retries.
Each controlled build retry has a separate private provider-neutral BuildAttempt containing a cryptographically strong attempt identity and bounded start/deadline metadata. The resolver persists the attempt before submission and uses that exact attempt for ambiguous-response and restart reconciliation. A genuine retry creates a new attempt without changing the stable cache request identity. Provider adapters translate attempt metadata to their own run/job model; provider run names, creation timestamps and handles do not enter the resolver contract or public MCP surface. Resolver persistence schema 2 stores only this private neutral attempt record plus the opaque provider handle.
READY requires all exact labels, a valid OCI source revision, and a canonical immutable sha256: image ID. A successful build or mutable tag is not readiness evidence by itself.
Flutter cache schema 3 adds the required io.safe-reverser.dart.os OCI label. Schema-2 images remain immutable and are not rewritten. Their migration path is deterministic rebuild under the schema-3 identity; the changed schema participates in the cache digest, so an older image cannot collide with or be silently reused as a schema-3 cache.
Controlled builders execute outside analyzer workers. Builder credentials are host-only and must never enter worker environment, analysis job metadata, evidence envelopes, MCP responses, provider-neutral state details, or raw error output.
Host filesystem operations use the shared Path SDK and reject:
- absolute user artifact paths where project-relative paths are required;
- lexical escape;
- resolved escape;
- symlinked path components;
- symlinked data roots;
- unsafe deletion targets;
- oversized metadata.
Directory roots are checked before creation/canonicalization so a symlinked parent is not intentionally traversed first and rejected afterward.
Current path policy addresses untrusted-artifact and ordinary symlink substitution. If hostile same-UID filesystem races enter the threat model, critical operations should move toward dirfd/openat/openat2-style primitives without changing Capability SPI.
Capability-specific jobs use the shared AnalysisJobStore:
<data-root>/<capability-id>/jobs/<12-hex-job-id>/
The store provides:
- non-predictable bounded IDs;
- private directories;
- atomic bounded metadata writes;
- metadata/directory identity checks;
- bounded returned job count;
- hard filesystem entry scan budget.
Capability-private artifacts/indexes may live in the job directory subject to capability-specific bounds.
Optimized analyzer storage remains private:
DEX SQLite
Flutter SQLite
future native/IR/Hermes/IL2CPP caches
The public compatibility descriptor includes:
capability id
Capability API
Worker ABI
operation
EvidenceEnvelope version
When valid material provenance is available, the control plane emits an EvidenceEnvelope with:
schema version
analysis id
artifact SHA-256
producer capability
producer version
evidence state
operation/provenance payload
limitations
Evidence state is strictly:
observed
derived
hypothesized
No numeric confidence is fabricated by the platform.
PEG remains the long-lived semantic model. New data-flow/security/dynamic/native capabilities add evidence/relations rather than replacing the shared evidence architecture.
Framework routing and deployment readiness are separate facts.
A route declares topology such as:
primary_capability_id = framework-flutter
The host control plane enriches the shared analysis_route shape with runtime state:
declared
installed
ready
degraded
unavailable
unsupported
A fingerprint worker must not claim an external analyzer image/runtime is ready merely because the framework is detected.
A normal new capability should require only:
- a validated manifest;
- an existing adapter kind, or a new narrowly scoped adapter registered behind the adapter factory;
- an isolated worker image/analyzer implementation;
- deterministic tests and capability-specific CI;
- shared provenance/evidence normalization;
- Worker ABI-compatible diagnostics.
It must not require:
- another public MCP;
- another generic Runtime Driver;
- another generic job store;
- another host path implementation;
- another evidence-state model;
- another top-level public health implementation;
- operation-name-specific dispatch branches;
- privilege expansion of unrelated capabilities.
Central platform CI validates invariants and release baseline requirements, not an exact forever set of all capability IDs.
For example, 0.3 may require at least:
static-core
framework-flutter
but a compatible future optional manifest must not fail solely because it is an additional capability.
Capability-specific build/integration checks may still explicitly name the capability they test.
Worker ABI v1 currently validates declared public operation ownership/tool names and diagnostics behavior.
That is sufficient for the 0.3 foundation but is not the final 1.0 compatibility policy.
Before 1.0, public semantic operations must gain a stable compatibility rule for:
- input schema changes;
- required/optional arguments;
- externally meaningful output fields;
- error/partial/unsupported semantics.
A future implementation may use per-operation contract versions, normalized schema hashes, or another deterministic compatibility mechanism. Operation-name equality alone must not be treated as a permanent 1.0 ABI guarantee.
The following require explicit platform review:
- Capability API change;
- Worker ABI change;
- EvidenceEnvelope breaking change;
- trust-boundary/activation semantic change;
- Runtime Driver privilege expansion;
- path/job security invariant change;
- operation ownership conflict;
- internal diagnostics ABI change;
- verified immutable-image execution change;
- runtime-cache identity change that could reuse incompatible code;
- externally breaking public operation schema change.
A capability-private parser/index may evolve without changing Capability API when externally observable semantics remain compatible.