diff --git a/plugins/clone-app/skills/clone-app/references/re-digest-contract.md b/plugins/clone-app/skills/clone-app/references/re-digest-contract.md new file mode 100644 index 0000000..0a000ed --- /dev/null +++ b/plugins/clone-app/skills/clone-app/references/re-digest-contract.md @@ -0,0 +1,85 @@ +# RE Digest Contract + +The Phase 2 subagent runs the reverse-engineering workflow in isolation and +MUST produce these three files under `$WORK/`, then return only the summary. +This file is the single source of truth for their schema. The subagent prompt +points here; do not duplicate the schema into SKILL.md prose. + +## File 1 — `$WORK/re-digest.md` (human-readable, main artifact) + +Required section headings, in this order: + +``` +# RE Digest — +## Framework & Stack framework, HTTP lib, DI, serialization, obfuscation level +## Hosts first-party vs third-party (table) +## Endpoint Inventory Tier-1: host | method | path | auth | source file +## Key Flow Payloads Tier-2: auth, payment/checkout, 1-2 core flows + — request body / response shape / headers / params +## BuildConfig Secrets base URLs, API keys, feature flags, flavors +## Feature Signals screen count, SDKs, permissions, components +## RE Method re-skill | direct-scripts | limited: +``` + +## File 2 — `$WORK/payloads.json` (machine-readable, durable memory) + +Required top-level keys: `package`, `re_method`, `endpoints`, `buildconfig`. +Each item in `endpoints` has: `host`, `method`, `path`, `auth`, `source`, +`request_body`, `response`, `headers`. + +```json +{ + "package": "com.example.app", + "re_method": "re-skill", + "endpoints": [ + { + "host": "api.example.com", + "method": "POST", + "path": "/v1/auth/login", + "auth": "none", + "source": "com/example/api/AuthApi.java", + "request_body": { "email": "string", "password": "string" }, + "response": { "token": "string", "user": {} }, + "headers": { "Content-Type": "application/json" } + } + ], + "buildconfig": { "BASE_URL": "https://api.example.com/v1" } +} +``` + +- `request_body` and `response` are `null` for Tier-1-only endpoints. +- Populate them ONLY for the key flows: **auth, payment/checkout, and the + 1–2 core feature endpoints** (decision Q3=B). Everything else is a Tier-1 + row with `null` payloads. Going Tier-2 on every endpoint is a non-goal — + it is token-expensive and the RE skill itself warns against it. + +## File 3 — `$WORK/re-summary.txt` (≤40 lines, the only RE text returned) + +Plain text. The subagent's return value = the contents of this file plus the +paths to the two files above. Fields: + +- framework +- host count (first-party / third-party) +- endpoint count +- key-flow names found (auth / payment / core) +- secrets-found count (BuildConfig) +- RE method: `re-skill` | `direct-scripts` | `limited: ` +- blockers / warnings (e.g. obfuscation, framework guard) + +The subagent MUST NOT return raw decompiled sources. + +## RE Method values + +| Value | Meaning | +|---|---| +| `re-skill` | The `android-reverse-engineering` skill ran the workflow. | +| `direct-scripts` | The skill was absent; the sibling plugin's bash scripts ran. | +| `limited: ` | Flutter / React Native / Cordova / Xamarin — Java decompile is shallow; payloads may be empty and the digest is partial. | + +## Framework guard + +If the fingerprint reports Flutter / React Native / Cordova / Xamarin, set +`RE Method: limited: `, write whatever partial signals are +available (manifest, strings, hardcoded URLs, SDK list), and leave payloads +empty where they cannot be recovered. Downstream phases widen the +uncertainty band accordingly. diff --git a/plugins/clone-app/tests/fixtures/payloads.sample.json b/plugins/clone-app/tests/fixtures/payloads.sample.json new file mode 100644 index 0000000..1cee721 --- /dev/null +++ b/plugins/clone-app/tests/fixtures/payloads.sample.json @@ -0,0 +1,27 @@ +{ + "package": "com.example.app", + "re_method": "re-skill", + "endpoints": [ + { + "host": "api.example.com", + "method": "POST", + "path": "/v1/auth/login", + "auth": "none", + "source": "com/example/api/AuthApi.java", + "request_body": { "email": "string", "password": "string" }, + "response": { "token": "string", "user": {} }, + "headers": { "Content-Type": "application/json" } + }, + { + "host": "api.example.com", + "method": "GET", + "path": "/v1/users/profile", + "auth": "Bearer", + "source": "com/example/api/UserApi.java", + "request_body": null, + "response": null, + "headers": { "Authorization": "Bearer " } + } + ], + "buildconfig": { "BASE_URL": "https://api.example.com/v1" } +} diff --git a/plugins/clone-app/tests/fixtures/re-digest.sample.md b/plugins/clone-app/tests/fixtures/re-digest.sample.md new file mode 100644 index 0000000..4e46e97 --- /dev/null +++ b/plugins/clone-app/tests/fixtures/re-digest.sample.md @@ -0,0 +1,38 @@ +# RE Digest — com.example.app + +## Framework & Stack +Native Kotlin · Retrofit + OkHttp · Hilt · kotlinx.serialization · obfuscation: moderate + +## Hosts +| Host | Party | +|------|-------| +| api.example.com | first | +| analytics.thirdparty.io | third | + +## Endpoint Inventory +| Host | Method | Path | Auth | Source | +|------|--------|------|------|--------| +| api.example.com | POST | /v1/auth/login | none | com/example/api/AuthApi.java | +| api.example.com | GET | /v1/users/profile | Bearer | com/example/api/UserApi.java | +| api.example.com | POST | /v1/orders | Bearer | com/example/api/OrderApi.java | + +## Key Flow Payloads +### POST /v1/auth/login (auth) +- request body: `{ "email": "string", "password": "string" }` +- response: `{ "token": "string", "user": {} }` +- headers: `Content-Type: application/json` + +### POST /v1/orders (payment/core) +- request body: `{ "items": [], "total": "number" }` +- response: `{ "order_id": "string", "status": "string" }` +- headers: `Authorization: Bearer ` + +## BuildConfig Secrets +- BASE_URL = https://api.example.com/v1 +- ANALYTICS_KEY = + +## Feature Signals +~18 screens · SDKs: Firebase, AppsFlyer · permissions: INTERNET, ACCESS_NETWORK_STATE + +## RE Method +re-skill diff --git a/plugins/clone-app/tests/test-re-digest-contract.sh b/plugins/clone-app/tests/test-re-digest-contract.sh new file mode 100644 index 0000000..e2d0657 --- /dev/null +++ b/plugins/clone-app/tests/test-re-digest-contract.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +P="$HERE/.." # plugins/clone-app +CONTRACT="$P/skills/clone-app/references/re-digest-contract.md" +DIGEST_FIX="$HERE/fixtures/re-digest.sample.md" +PAYLOAD_FIX="$HERE/fixtures/payloads.sample.json" +fail=0 +has() { grep -qF "$2" "$1" && echo "PASS: $3" || { echo "FAIL: $3 — '$2' not in ${1##*/}"; fail=1; }; } + +# Contract doc exists and documents every required re-digest.md section heading +for sec in "## Framework & Stack" "## Hosts" "## Endpoint Inventory" \ + "## Key Flow Payloads" "## BuildConfig Secrets" "## Feature Signals" "## RE Method"; do + has "$CONTRACT" "$sec" "contract documents section $sec" +done + +# Contract doc documents every required payloads.json key +for key in '"package"' '"re_method"' '"endpoints"' '"buildconfig"' \ + '"request_body"' '"response"' '"headers"'; do + has "$CONTRACT" "$key" "contract documents json key $key" +done + +# Contract names the three output files and the three RE Method values +for tok in "re-digest.md" "payloads.json" "re-summary.txt" \ + "re-skill" "direct-scripts" "limited:"; do + has "$CONTRACT" "$tok" "contract names token $tok" +done + +# Digest fixture has every required section heading +for sec in "## Framework & Stack" "## Hosts" "## Endpoint Inventory" \ + "## Key Flow Payloads" "## BuildConfig Secrets" "## Feature Signals" "## RE Method"; do + has "$DIGEST_FIX" "$sec" "digest fixture has section $sec" +done + +# Payload fixture is valid JSON and has the required shape +python3 -c " +import json,sys +d=json.load(open('$PAYLOAD_FIX')) +for k in ('package','re_method','endpoints','buildconfig'): + assert k in d, 'missing top key '+k +assert isinstance(d['endpoints'],list) and d['endpoints'], 'endpoints must be non-empty list' +for e in d['endpoints']: + for k in ('host','method','path','auth','source','request_body','response','headers'): + assert k in e, 'endpoint missing key '+k +print('PASS: payload fixture shape valid') +" || { echo "FAIL: payload fixture shape"; fail=1; } + +exit $fail