{"owner":"shiaho777","repo":"web-to-app","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nInstructions for coding agents working in this repository.\n\n## Code style\n\n- Do what you believe is right. Make the change complete and correct, not the smallest possible diff. If a fix calls for refactoring, renaming, or touching multiple files, do it.\n- Match the patterns and conventions already in the surrounding code.\n- Do not add copyright or license headers unless asked.\n\n## Project layout\n\n| Path | Role |\n|------|------|\n| `app/` | Full builder host: editor UI, export pipeline, runtimes, preview. |\n| `shell/` | Runtime template. Built to `app/src/main/assets/template/webview_shell.apk` via `:shell:assembleRelease` + `:app:syncShellTemplateApk`. |\n| `clone-host/` | Host-side APK clone / identity reshape support library (compiled to a DEX asset). |\n| `modules/` | Module Market catalog (`registry.json` + per-module folders). |\n| `docs/` | VitePress documentation site (guide / developer / extensions, EN + ZH), published to https://shiaho777.github.io/web-to-app/ by `.github/workflows/docs-deploy.yml`. Site URL paths map 1:1 to files under `docs/` (`/zh/...` → `docs/zh/...`). |\n| `scripts/` | Build helpers and gates (`check_config_field_drift.py`). |\n\nRuntime Kotlin is authored under `app/` and synced into `shell` by `syncShellRuntimeSources`. Edit the `app/` source once; do not permanently fork copies under shell.\n\nUser-facing product docs: `README.md`, `.github/docs/README_CN.md`, `.github/CONTRIBUTING.md`, `modules/README.md`. The published documentation site is https://shiaho777.github.io/web-to-app/ (source: `docs/`, deployed by `.github/workflows/docs-deploy.yml`).\n\n## How the main pieces connect\n\n```text\nEditor (Compose screens in app/)\n  ↔ data models (WebApp, configs)\n  ↔ export factory (ApkConfig / ApkConfigJsonFactory)\n  ↔ ApkBuilder / ApkBuildCache  →  signed generated APK\n\napp/ sources\n  → syncShellRuntimeSources  →  shell DEX  →  webview_shell.apk (template)\n\nGenerated APK runtime\n  WebToAppApplication → ShellModeManager → load assets JSON config\n  → WebViewManager / runtime servers (Node/PHP/Python/Go/WordPress)\n```\n\nMental model:\n\n- **Host preview** runs `:app` with all classes on the main classpath.\n- **Generated APK** runs the shell template classes (full runtime synced from `app/`), reading config from assets JSON via `ShellModeManager`.\n- A flag in the editor is useless at export unless it flows through **model → ApkConfig JSON → shell config → runtime code**.\n\n## i18n\n\n- Host UI strings live in `app/src/main/java/com/webtoapp/core/i18n/Strings.kt` (split across `Strings` / `StringsA` … `StringsE`).\n- **All** user-visible strings must be inline `when (Strings.lang)` blocks covering all 10 languages: Chinese, English, Arabic, Portuguese, Spanish, French, German, Russian, Japanese, Korean. `when(lang)` blocks may never use `else ->` — `AppStringsResourceConsistencyTest` and `StringsKtTranslationParityTest` enforce this.\n- **Never** load user-visible text via `context.getString(R.string.*)` / `stringResource(R.string.*)`. `res/values*/` is only maintained for zh/en/ar; the other 7 locales have no `values-*/` directory, so resource lookups silently fall back to the default `values/` (Chinese). Use `Strings.xxx` (or `Strings.funName(arg)` for parameterised strings — see `linuxEnvInstalledToast(name)` for the pattern). A test gates this: `kotlin source never references R string for user-visible text`.\n- `R.string` is reserved for `translatable=\"false\"` non-localised resources only (e.g. `app_name`).\n- Prefer adding properties on the existing split objects; match surrounding style.\n\n## Android and packaging constraints\n\n- Generated apps keep a low `targetSdk` (28) on the shell path because they rely on on-device fork+exec runtimes. Do not raise shell targetSdk casually.\n- The host app targets SDK 35 (antivirus reputation); SELinux W^X therefore blocks host-side exec of downloaded runtimes. `RuntimeExecPolicy` (`core/linux`) gates those previews by probing the installed targetSdk — generated APKs (always 28) pass unconditionally. Node preview (JNI via native libs) is unaffected.\n- Avoid new third-party dependencies unless strongly justified (`app/build.gradle.kts` / `shell/build.gradle.kts`). Prefer platform APIs and existing modules.\n- Notification push channels: Web Notification polyfill, polling, WebSocket, FCM (developer-owned Firebase config). Do not add OEM vendor push SDKs by default.\n- Foreground services and notification helpers must use `SafeNotificationChannels` (or equivalent fail-soft create). Channel creation failures must not crash FGS startup.\n- **One shell template:** `webview_shell.apk` from `:shell` release. Do not introduce a second template APK.\n- Export incremental rebuild lives in `app/.../apkbuilder` (`ApkBuildCache` + `ApkBuilder`):\n  - Modes: `FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`.\n  - Template / entry identities must be **content-stable** (no mtime-based keys).\n  - Encrypted builds always force a full rebuild.\n  - Do not feed signed or renamed APKs back into full `modifyApk` as templates.\n- Port coordination: `PortManager` + `PortConflictMode` (`REASSIGN` / `AUTO_KILL` / `ALERT`) with real stop handlers. Local server runtimes must allocate through PortManager and clean up on stop.\n- Local server / Linux env DNS: fork+exec runtimes (Node / PHP / Python / Go / WordPress / Linux env) should wire through `LocalDnsBridgeProxy` when they need host DNS/proxy bridging.\n- Large runtime downloads use `NetworkModule.downloadClient` (extended timeouts), not the default short-lived client.\n- HTML / FRONTEND packaged shells need file-scheme access via `ShellWebViewConfig` (`allowFileAccess` / local-file detection). Do not regress pure file-based HTML loads.\n- Node.js export must embed `libnode_bridge.so`, `libnode.so` (16KB-aligned), and `libc++_shared.so` as native libs. Go export must embed `libgo_exec_loader.so`.\n- Gradle custom tasks (`syncCloneHostDex`, etc.) must be configuration-cache safe: capture `File`/`Provider` values at configuration time, do not reference `Project`/`android.sdkDirectory` inside task closures.\n\n## Workflow\n\n- Do not commit secrets, `local.properties`, keystores, or IDE/cache junk.\n- Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).\n- When changing export or shell packaging, rebuild the template you touched.\n- When changing config fields, run `checkConfigFieldDrift` to catch model ↔ shell config name drift.\n\n### Delivery (Issue + PR + CI)\n\nDefault target: [shiaho777/web-to-app](https://github.com/shiaho777/web-to-app). Prefer a pull request over direct pushes to `main` when delivering code. Human-facing wording of the same loop lives in [CONTRIBUTING.md](.github/CONTRIBUTING.md); keep those docs in sync when this process changes.\n\n**Language (required):** GitHub **Issues and PRs must be written in English** — titles, bodies, labels text you author, and delivery comments on the Issue/PR. Local chat with the user may be Chinese or any language; do not copy that language into Issue/PR text.\n\nWhen the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end. Do not close the Issue until the PR is merged and CI is green.\n\n---\n\n## Shell template and runtime sync\n\n### Dual runtime (preview vs export)\n\n| | Host `:app` | Generated APK |\n|--|-------------|---------------|\n| DEX | All `app/src` classes | Shell sync include−exclude (full runtime set) |\n| Config | Editor / in-memory models | Assets JSON via `ShellModeManager` |\n| Template | Not used | `app/src/main/assets/template/webview_shell.apk` |\n\n**Preview ≠ export** unless both paths stay valid:\n\n1. Host still has the real implementation.\n2. Shell-synced code reads the config field from assets JSON at runtime.\n3. The field name in `ApkConfig` JSON matches the `@SerializedName` in `ShellModeManager` (Gson silently drops mismatches).\n\nMost common failure: preview works; exported APK silently skips the feature because a config field name drifted between the export factory and the shell config class.\n\n### Where each concern is edited\n\n| Concern | Path |\n|---------|------|\n| What enters shell | `shell/build.gradle.kts` → `syncShellRuntimeSources` include/exclude |\n| Shell template build | `:shell:assembleRelease` + `:app:syncShellTemplateApk` |\n| Template output | `app/src/main/assets/template/webview_shell.apk` |\n| Config → shell JSON | `app/.../apkbuilder/ApkConfigJsonFactory.kt` |\n| Shell config types | `app/.../core/shell/ShellModeManager.kt` |\n| Runtime WebView config | `app/.../ui/shell/ShellWebViewConfig.kt` |\n| Config drift gate | `scripts/check_config_field_drift.py` → `:app:checkConfigFieldDrift` |\n| Shell minify policy | `shell/proguard-rules.pro` |\n\n---\n\n## Common change recipes\n\nThese are the default approaches for everyday work. Follow the chain end-to-end; stopping at UI or host-only code is how preview and export diverge.\n\n### 1. Add or change a host UI string\n\n1. Add the property in the correct `Strings*` split with all 10 languages.\n2. Reference it from Compose/UI the same way neighbors do.\n\n### 2. Add an editor setting that must affect the generated APK\n\nTrace and update **all** of:\n\n1. Model (`WebApp` / nested config) and editor UI binding\n2. Export mapping (`ApkBuilder` / `ApkConfig` / `ApkConfigJsonFactory`)\n3. Shell config types (`ShellModeManager` / shell config data classes) if runtime reads them\n4. Runtime use site in shell-synced code\n5. Unit tests for export wiring when flags change\n6. **Coverage test update (REQUIRED).** When adding a new Boolean field to\n   `WebViewConfig`, you MUST add it to `flipAllBooleans()` in\n   `WebViewConfigBooleanCoverageTest.kt`. The test\n   `flipAllBooleans covers every declared Boolean field` uses reflection to\n   verify that every declared Boolean field is listed — if you forget, CI\n   fails with a message naming the missing field. For non-Boolean fields, add\n   a spot-check to `key non-Boolean WebViewConfig fields survive the export\n   round-trip`. This is the safety net that catches \"preview works, export\n   broken\" before it ships.\n\nMissing any step usually yields: editor shows the switch, export ignores it, or export embeds config the runtime never reads.\n\n### 3. Change shell runtime behavior used by every generated app\n\n1. Edit the source under `app/` (shared runtime).\n2. Confirm the file is included by `syncShellRuntimeSources`.\n3. Rebuild shell template if you need to validate packaging.\n4. Keep changes surgical; shell has a low targetSdk and a thin dependency set.\n5. If you touch FGS / notification channel creation, fail soft via `SafeNotificationChannels`.\n\n### 4. Add a host-only feature (editor, market, tooling)\n\n1. Keep implementation under host-only packages (`core/apkbuilder`, host screens, sample/market, `core/host`, …).\n2. Do not pull host-only deps into `shell/build.gradle.kts`.\n\n### 5. Touch APK export or incremental rebuild\n\n1. Prefer `ApkBuildCache` / content hashes over timestamps.\n2. Encrypted builds stay full rebuild.\n3. Do not use signed outputs as templates.\n4. If template bytes change, ensure cache keys invalidate correctly.\n\n### 6. Change notifications / engines / network hardening\n\n1. Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).\n2. Do not add OEM push SDKs by default.\n3. FGS channel create paths must tolerate OEM/channel failures.\n\n### 7. Module Market / `modules/`\n\n1. Follow `modules/README.md` catalog layout (`registry.json` + module folders).\n2. Runtime consumption still goes through the extension/shell paths if it ships inside generated APKs.\n\n### 8. Fix \"works in preview, broken after export\"\n\nChecklist in order:\n\n1. Did shell config JSON actually contain the field at runtime?\n2. Does the field name in `ApkConfig` JSON match the `@SerializedName` in shell config? Run `checkConfigFieldDrift`.\n3. Is the runtime use site shell-synced (not host-only)?\n4. For adblock: confirm `adBlockEnabled` mapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state.\n5. Rebuild template after sync changes (stale template is a frequent miss).\n\n### 9. Local server runtime / download path\n\n1. Allocate ports through `PortManager` with the configured conflict policy; implement real stop handlers.\n2. Wire fork+exec processes into `LocalDnsBridgeProxy` when they need host DNS/proxy env.\n3. Use `NetworkModule.downloadClient` for large dependency / engine / runtime downloads.\n\n### 10. Node.js / Go export\n\n1. Node.js: ensure `injectNodeJsNativeLibs` embeds `libnode_bridge.so` + `libnode.so` (16KB-aligned via `ElfAligner16k`) + `libc++_shared.so`. Node binary resolution prefers `nativeLibraryDir`, falls back to download cache.\n2. Go: ensure `injectGoExecLoaderNativeLib` embeds `libgo_exec_loader.so`.\n3. `NodeService` runs in a dedicated `:nodejs` OS process so V8 lifecycle is isolated from the host.\n\n### 11. Change a feature that has an Agent tool\n\nThe in-app Agent exposes 40+ tools that wrap host service classes. When you change a feature, trace the tool chain:\n\n```text\nLLM response (tool_calls)\n  → AgentEngine.executeToolCall()\n  → PermissionPrompter (write tools ask user; read-only run silently)\n  → Tool.execute(args, ctx)\n  → Service class (ApkBuilder, AppExporter, PortManager, EngineManager, AdBlocker, …)\n  → ToolResult → back to LLM\n```\n\nChecklist when touching a feature that an Agent tool wraps:\n\n1. **API signature drift.** If the service class constructor, method signature, or return type changes, update the tool's `execute()` in `app/.../agent/tool/builtin/`. A stale call compiles only if the old overload still exists; otherwise it's a build break.\n2. **`parametersSchema` ↔ `execute()` alignment.** The JSON schema advertised to the LLM must match what `execute()` actually reads from `args`. Adding a parameter to the service call without exposing it in the schema means the LLM can never pass it.\n3. **`description` accuracy.** The LLM decides *when* and *how* to call a tool solely from `description` + `parametersSchema`. If the feature's behavior changed, update the description text — a misleading description causes silent misuse.\n4. **`isReadOnly()` correctness.** `true` → runs without user confirmation; `false` → triggers `PermissionPrompter` dialog. If a tool gains write side-effects, flip it to `false`.\n5. **Registration.** New tools must be added to `ToolRegistryFactory.baseTools()` (grouped by domain comment). Forgotten registration = tool invisible to the LLM.\n6. **Removed features.** If a feature is deleted, remove or disable its tool. A tool that calls a dead class crashes at runtime inside the agent loop.\n7. **Host-only.** Agent tools live under `core/agent/` and are never shell-synced. Do not add them to `syncShellRuntimeSources`.\n\nKey paths:\n\n| Concern | Path |\n|---------|------|\n| Tool interface | `app/.../agent/tool/Tool.kt` |\n| Tool registry | `app/.../agent/tool/ToolRegistryFactory.kt` |\n| Tool context (services access) | `app/.../agent/tool/ToolContext.kt` |\n| Built-in tools (by domain) | `app/.../agent/tool/builtin/*.kt` |\n| Agent loop / execution | `app/.../agent/engine/AgentEngine.kt` |\n| Permission prompt (Channel-based) | `app/.../agent/permission/PermissionPrompter.kt` |\n| LLM provider (SSE streaming) | `app/.../agent/llm/OpenAiCompatProvider.kt` |\n\n---\n\n## Easy-to-miss points\n\n- **Shared sources are authored in `app/`.** Editing only a file under `shell/src` is usually wrong; it will be overwritten on sync or diverge from host.\n- **Config field names drift.** Editor model, `ApkConfig`, JSON factory, and shell config must stay aligned; Gson silently drops unknown/missing fields. Run `checkConfigFieldDrift`.\n- **Low targetSdk (28) and fork/exec runtimes** constrain \"modernize the shell SDK\" changes.\n- **Incremental export cache** keys must be content-based; mtime and resigned APKs create false hits/misses.\n- **HTML/FRONTEND file access.** Packaged local-file shells must have `allowFileAccess = true` (forced in `buildWebViewBlock` and `ShellWebViewConfig`); do not regress pure file-based HTML loads.\n- **Node native libs.** Exported NODEJS_APP needs `libnode_bridge.so` + `libnode.so` + `libc++_shared.so`; missing any causes `loadNode` / `loadJniBridge` failure at runtime.\n- **16KB page alignment.** `libnode.so` and other large ELF natives must be 16KB-aligned (`ElfAligner16k`) for Android 15+ devices; `node_bridge.cpp` / `node_launcher.c` enable 16KB app-compat before `dlopen`.\n- **Node JNI output bridge.** `NodeJniOutputBridge` is a stable class referenced by native code; keep its `-keep` proguard rule so R8 does not rename `onOutput`.\n- **Crashing FGS when notification channel creation fails.** Always use `SafeNotificationChannels` for channel creation.\n- **Adblock is wired for preview + export.** Do not wipe host filter state during export; the host AdBlocker serves preview and the compiled rule set ships in the APK.\n- **Runtime permissions are feature-driven.** `RuntimePermissionSync` derives the permission list from enabled features; do not revert to a static template.\n- **Splash preview media path.** Preview reads splash media from the host filesystem (`splashMediaPath`); export packages it into assets. Do not hardcode `assets/splash_media.*` as the only source.\n- **Port conflict policy.** Local server runtimes must allocate through `PortManager` and clean up on stop; do not bind ports directly.\n- **Agent tool ↔ service drift.** When a service class API changes, the corresponding Agent tool in `core/agent/tool/builtin/` must be updated in the same PR. A stale tool either fails to compile or silently passes wrong arguments at runtime. Check `ToolRegistryFactory.baseTools()` for the full tool list.\n\n---\n\n## Forbidden / high-risk mistakes\n\n- Second shell template APK\n- Excluding a shell-synced class but leaving imports/constructors in shell-synced sources\n- Re-enabling shell R8 obfuscation / aggressive shrink without testing exported apps\n- Putting host-only tools back into shell sync for convenience\n- OEM push SDKs or unjustified heavy dependencies in shell\n- Feeding signed/renamed APKs into template/modify paths\n- Crashing FGS when notification channel creation fails\n- Committing secrets, keystores, or local machine config\n- Regressing HTML/FRONTEND file access in packaged shells\n- Shipping NODEJS_APP without `libnode_bridge.so` / `libnode.so` / `libc++_shared.so`\n- Skipping 16KB alignment for large ELF natives\n\n---\n\n## Verify commands\n\n```bash\n./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache\n./gradlew :app:compileDebugKotlin -x syncCloneHostDex --no-configuration-cache\n./gradlew :app:checkConfigFieldDrift --no-configuration-cache\npython3 scripts/check_config_field_drift.py\n```\n\nUse these when you change shell membership, export packaging, or config fields. For host-only UI/string work, targeted compile on `:app` is usually enough.\n\nRelated focused tests often worth running after nearby edits: `ApkBuildCacheTest`, `AdBlockerHostRuntimeTest`, `AdBlockExportWiringTest`, `PortManagerTest`, `BuildInputPreflightTest`, `GoBuildEnvironmentTest`, `RuntimePermissionSyncTest`.\n\n---\n\n## Implementation snapshot\n\nLanded:\n\n- Single shell template (`webview_shell.apk`) from `:shell` release, full runtime synced from `app/`\n- Incremental `ApkBuildCache` (`FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`); encrypted builds always full\n- Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions\n- `SafeNotificationChannels` fail-soft path for FGS\n- `PortManager` conflict policies + real stop handlers across Node/PHP/Python/Go/WordPress\n- `LocalDnsBridgeProxy` wiring for local server runtimes (including Node.js)\n- Runtime downloads via `NetworkModule.downloadClient`\n- Adblock preview + export wiring restored\n- HTML/FRONTEND file-access for packaged local shells\n- Node.js export: `libnode_bridge.so` + 16KB-aligned `libnode.so` + `libc++_shared.so`; 16KB app-compat before dlopen; stable `NodeJniOutputBridge` JNI callback\n- Go export: `libgo_exec_loader.so` embedded; in-app build ENOSPC handling + GOTMPDIR relocation\n- Runtime permission sync (feature-driven)\n- Splash preview media path fallback\n- Config field drift detection (`checkConfigFieldDrift`)\n- Module Market: Chrome Web Store live search + GreasyFork browse\n- Code editor find-and-replace\n- Agent tool system: 40+ tools (app lifecycle, ports/engine, hosts/runtime, stats/modifier/import, build env/Play, modules, files) with Channel-based permission prompting, per-section SSE parse resilience, and plan mode\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nInstructions for coding agents working in this repository.\n\n## Code style\n\n- Do what you believe is right. Make the change complete and correct, not the smallest possible diff. If a fix calls for refactoring, renaming, or touching multiple files, do it.\n- Match the patterns and conventions already in the surrounding code.\n- Do not add copyright or license headers unless asked.\n\n## Project layout\n\n| Path | Role |\n|------|------|\n| `app/` | Full builder host: editor UI, export pipeline, runtimes, preview. |\n| `shell/` | Runtime template. Built to `app/src/main/assets/template/webview_shell.apk` via `:shell:assembleRelease` + `:app:syncShellTemplateApk`. |\n| `clone-host/` | Host-side APK clone / identity reshape support library (compiled to a DEX asset). |\n| `modules/` | Module Market catalog (`registry.json` + per-module folders). |\n| `docs/` | VitePress documentation site (guide / developer / extensions, EN + ZH), published to https://shiaho777.github.io/web-to-app/ by `.github/workflows/docs-deploy.yml`. Site URL paths map 1:1 to files under `docs/` (`/zh/...` → `docs/zh/...`). |\n| `scripts/` | Build helpers and gates (`check_config_field_drift.py`). |\n\nRuntime Kotlin is authored under `app/` and synced into `shell` by `syncShellRuntimeSources`. Edit the `app/` source once; do not permanently fork copies under shell.\n\nUser-facing product docs: `README.md`, `.github/docs/README_CN.md`, `.github/CONTRIBUTING.md`, `modules/README.md`. The published documentation site is https://shiaho777.github.io/web-to-app/ (source: `docs/`, deployed by `.github/workflows/docs-deploy.yml`).\n\n## How the main pieces connect\n\n```text\nEditor (Compose screens in app/)\n  ↔ data models (WebApp, configs)\n  ↔ export factory (ApkConfig / ApkConfigJsonFactory)\n  ↔ ApkBuilder / ApkBuildCache  →  signed generated APK\n\napp/ sources\n  → syncShellRuntimeSources  →  shell DEX  →  webview_shell.apk (template)\n\nGenerated APK runtime\n  WebToAppApplication → ShellModeManager → load assets JSON config\n  → WebViewManager / runtime servers (Node/PHP/Python/Go/WordPress)\n```\n\nMental model:\n\n- **Host preview** runs `:app` with all classes on the main classpath.\n- **Generated APK** runs the shell template classes (full runtime synced from `app/`), reading config from assets JSON via `ShellModeManager`.\n- A flag in the editor is useless at export unless it flows through **model → ApkConfig JSON → shell config → runtime code**.\n\n## i18n\n\n- Host UI strings live in `app/src/main/java/com/webtoapp/core/i18n/Strings.kt` (split across `Strings` / `StringsA` … `StringsE`).\n- **All** user-visible strings must be inline `when (Strings.lang)` blocks covering all 10 languages: Chinese, English, Arabic, Portuguese, Spanish, French, German, Russian, Japanese, Korean. `when(lang)` blocks may never use `else ->` — `AppStringsResourceConsistencyTest` and `StringsKtTranslationParityTest` enforce this.\n- **Never** load user-visible text via `context.getString(R.string.*)` / `stringResource(R.string.*)`. `res/values*/` is only maintained for zh/en/ar; the other 7 locales have no `values-*/` directory, so resource lookups silently fall back to the default `values/` (Chinese). Use `Strings.xxx` (or `Strings.funName(arg)` for parameterised strings — see `linuxEnvInstalledToast(name)` for the pattern). A test gates this: `kotlin source never references R string for user-visible text`.\n- `R.string` is reserved for `translatable=\"false\"` non-localised resources only (e.g. `app_name`).\n- Prefer adding properties on the existing split objects; match surrounding style.\n\n## Android and packaging constraints\n\n- Generated apps keep a low `targetSdk` (28) on the shell path because they rely on on-device fork+exec runtimes. Do not raise shell targetSdk casually.\n- The host app targets SDK 35 (antivirus reputation); SELinux W^X therefore blocks host-side exec of downloaded runtimes. `RuntimeExecPolicy` (`core/linux`) gates those previews by probing the installed targetSdk — generated APKs (always 28) pass unconditionally. Node preview (JNI via native libs) is unaffected.\n- Avoid new third-party dependencies unless strongly justified (`app/build.gradle.kts` / `shell/build.gradle.kts`). Prefer platform APIs and existing modules.\n- Notification push channels: Web Notification polyfill, polling, WebSocket, FCM (developer-owned Firebase config). Do not add OEM vendor push SDKs by default.\n- Foreground services and notification helpers must use `SafeNotificationChannels` (or equivalent fail-soft create). Channel creation failures must not crash FGS startup.\n- **One shell template:** `webview_shell.apk` from `:shell` release. Do not introduce a second template APK.\n- Export incremental rebuild lives in `app/.../apkbuilder` (`ApkBuildCache` + `ApkBuilder`):\n  - Modes: `FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`.\n  - Template / entry identities must be **content-stable** (no mtime-based keys).\n  - Encrypted builds always force a full rebuild.\n  - Do not feed signed or renamed APKs back into full `modifyApk` as templates.\n- Port coordination: `PortManager` + `PortConflictMode` (`REASSIGN` / `AUTO_KILL` / `ALERT`) with real stop handlers. Local server runtimes must allocate through PortManager and clean up on stop.\n- Local server / Linux env DNS: fork+exec runtimes (Node / PHP / Python / Go / WordPress / Linux env) should wire through `LocalDnsBridgeProxy` when they need host DNS/proxy bridging.\n- Large runtime downloads use `NetworkModule.downloadClient` (extended timeouts), not the default short-lived client.\n- HTML / FRONTEND packaged shells need file-scheme access via `ShellWebViewConfig` (`allowFileAccess` / local-file detection). Do not regress pure file-based HTML loads.\n- Node.js export must embed `libnode_bridge.so`, `libnode.so` (16KB-aligned), and `libc++_shared.so` as native libs. Go export must embed `libgo_exec_loader.so`.\n- Gradle custom tasks (`syncCloneHostDex`, etc.) must be configuration-cache safe: capture `File`/`Provider` values at configuration time, do not reference `Project`/`android.sdkDirectory` inside task closures.\n\n## Workflow\n\n- Do not commit secrets, `local.properties`, keystores, or IDE/cache junk.\n- Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).\n- When changing export or shell packaging, rebuild the template you touched.\n- When changing config fields, run `checkConfigFieldDrift` to catch model ↔ shell config name drift.\n\n### Delivery (Issue + PR + CI)\n\nDefault target: [shiaho777/web-to-app](https://github.com/shiaho777/web-to-app). Prefer a pull request over direct pushes to `main` when delivering code. Human-facing wording of the same loop lives in [CONTRIBUTING.md](.github/CONTRIBUTING.md); keep those docs in sync when this process changes.\n\n**Language (required):** GitHub **Issues and PRs must be written in English** — titles, bodies, labels text you author, and delivery comments on the Issue/PR. Local chat with the user may be Chinese or any language; do not copy that language into Issue/PR text.\n\nWhen the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end. Do not close the Issue until the PR is merged and CI is green.\n\n---\n\n## Shell template and runtime sync\n\n### Dual runtime (preview vs export)\n\n| | Host `:app` | Generated APK |\n|--|-------------|---------------|\n| DEX | All `app/src` classes | Shell sync include−exclude (full runtime set) |\n| Config | Editor / in-memory models | Assets JSON via `ShellModeManager` |\n| Template | Not used | `app/src/main/assets/template/webview_shell.apk` |\n\n**Preview ≠ export** unless both paths stay valid:\n\n1. Host still has the real implementation.\n2. Shell-synced code reads the config field from assets JSON at runtime.\n3. The field name in `ApkConfig` JSON matches the `@SerializedName` in `ShellModeManager` (Gson silently drops mismatches).\n\nMost common failure: preview works; exported APK silently skips the feature because a config field name drifted between the export factory and the shell config class.\n\n### Where each concern is edited\n\n| Concern | Path |\n|---------|------|\n| What enters shell | `shell/build.gradle.kts` → `syncShellRuntimeSources` include/exclude |\n| Shell template build | `:shell:assembleRelease` + `:app:syncShellTemplateApk` |\n| Template output | `app/src/main/assets/template/webview_shell.apk` |\n| Config → shell JSON | `app/.../apkbuilder/ApkConfigJsonFactory.kt` |\n| Shell config types | `app/.../core/shell/ShellModeManager.kt` |\n| Runtime WebView config | `app/.../ui/shell/ShellWebViewConfig.kt` |\n| Config drift gate | `scripts/check_config_field_drift.py` → `:app:checkConfigFieldDrift` |\n| Shell minify policy | `shell/proguard-rules.pro` |\n\n---\n\n## Common change recipes\n\nThese are the default approaches for everyday work. Follow the chain end-to-end; stopping at UI or host-only code is how preview and export diverge.\n\n### 1. Add or change a host UI string\n\n1. Add the property in the correct `Strings*` split with all 10 languages.\n2. Reference it from Compose/UI the same way neighbors do.\n\n### 2. Add an editor setting that must affect the generated APK\n\nTrace and update **all** of:\n\n1. Model (`WebApp` / nested config) and editor UI binding\n2. Export mapping (`ApkBuilder` / `ApkConfig` / `ApkConfigJsonFactory`)\n3. Shell config types (`ShellModeManager` / shell config data classes) if runtime reads them\n4. Runtime use site in shell-synced code\n5. Unit tests for export wiring when flags change\n6. **Coverage test update (REQUIRED).** When adding a new Boolean field to\n   `WebViewConfig`, you MUST add it to `flipAllBooleans()` in\n   `WebViewConfigBooleanCoverageTest.kt`. The test\n   `flipAllBooleans covers every declared Boolean field` uses reflection to\n   verify that every declared Boolean field is listed — if you forget, CI\n   fails with a message naming the missing field. For non-Boolean fields, add\n   a spot-check to `key non-Boolean WebViewConfig fields survive the export\n   round-trip`. This is the safety net that catches \"preview works, export\n   broken\" before it ships.\n\nMissing any step usually yields: editor shows the switch, export ignores it, or export embeds config the runtime never reads.\n\n### 3. Change shell runtime behavior used by every generated app\n\n1. Edit the source under `app/` (shared runtime).\n2. Confirm the file is included by `syncShellRuntimeSources`.\n3. Rebuild shell template if you need to validate packaging.\n4. Keep changes surgical; shell has a low targetSdk and a thin dependency set.\n5. If you touch FGS / notification channel creation, fail soft via `SafeNotificationChannels`.\n\n### 4. Add a host-only feature (editor, market, tooling)\n\n1. Keep implementation under host-only packages (`core/apkbuilder`, host screens, sample/market, `core/host`, …).\n2. Do not pull host-only deps into `shell/build.gradle.kts`.\n\n### 5. Touch APK export or incremental rebuild\n\n1. Prefer `ApkBuildCache` / content hashes over timestamps.\n2. Encrypted builds stay full rebuild.\n3. Do not use signed outputs as templates.\n4. If template bytes change, ensure cache keys invalidate correctly.\n\n### 6. Change notifications / engines / network hardening\n\n1. Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).\n2. Do not add OEM push SDKs by default.\n3. FGS channel create paths must tolerate OEM/channel failures.\n\n### 7. Module Market / `modules/`\n\n1. Follow `modules/README.md` catalog layout (`registry.json` + module folders).\n2. Runtime consumption still goes through the extension/shell paths if it ships inside generated APKs.\n\n### 8. Fix \"works in preview, broken after export\"\n\nChecklist in order:\n\n1. Did shell config JSON actually contain the field at runtime?\n2. Does the field name in `ApkConfig` JSON match the `@SerializedName` in shell config? Run `checkConfigFieldDrift`.\n3. Is the runtime use site shell-synced (not host-only)?\n4. For adblock: confirm `adBlockEnabled` mapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state.\n5. Rebuild template after sync changes (stale template is a frequent miss).\n\n### 9. Local server runtime / download path\n\n1. Allocate ports through `PortManager` with the configured conflict policy; implement real stop handlers.\n2. Wire fork+exec processes into `LocalDnsBridgeProxy` when they need host DNS/proxy env.\n3. Use `NetworkModule.downloadClient` for large dependency / engine / runtime downloads.\n\n### 10. Node.js / Go export\n\n1. Node.js: ensure `injectNodeJsNativeLibs` embeds `libnode_bridge.so` + `libnode.so` (16KB-aligned via `ElfAligner16k`) + `libc++_shared.so`. Node binary resolution prefers `nativeLibraryDir`, falls back to download cache.\n2. Go: ensure `injectGoExecLoaderNativeLib` embeds `libgo_exec_loader.so`.\n3. `NodeService` runs in a dedicated `:nodejs` OS process so V8 lifecycle is isolated from the host.\n\n### 11. Change a feature that has an Agent tool\n\nThe in-app Agent exposes 40+ tools that wrap host service classes. When you change a feature, trace the tool chain:\n\n```text\nLLM response (tool_calls)\n  → AgentEngine.executeToolCall()\n  → PermissionPrompter (write tools ask user; read-only run silently)\n  → Tool.execute(args, ctx)\n  → Service class (ApkBuilder, AppExporter, PortManager, EngineManager, AdBlocker, …)\n  → ToolResult → back to LLM\n```\n\nChecklist when touching a feature that an Agent tool wraps:\n\n1. **API signature drift.** If the service class constructor, method signature, or return type changes, update the tool's `execute()` in `app/.../agent/tool/builtin/`. A stale call compiles only if the old overload still exists; otherwise it's a build break.\n2. **`parametersSchema` ↔ `execute()` alignment.** The JSON schema advertised to the LLM must match what `execute()` actually reads from `args`. Adding a parameter to the service call without exposing it in the schema means the LLM can never pass it.\n3. **`description` accuracy.** The LLM decides *when* and *how* to call a tool solely from `description` + `parametersSchema`. If the feature's behavior changed, update the description text — a misleading description causes silent misuse.\n4. **`isReadOnly()` correctness.** `true` → runs without user confirmation; `false` → triggers `PermissionPrompter` dialog. If a tool gains write side-effects, flip it to `false`.\n5. **Registration.** New tools must be added to `ToolRegistryFactory.baseTools()` (grouped by domain comment). Forgotten registration = tool invisible to the LLM.\n6. **Removed features.** If a feature is deleted, remove or disable its tool. A tool that calls a dead class crashes at runtime inside the agent loop.\n7. **Host-only.** Agent tools live under `core/agent/` and are never shell-synced. Do not add them to `syncShellRuntimeSources`.\n\nKey paths:\n\n| Concern | Path |\n|---------|------|\n| Tool interface | `app/.../agent/tool/Tool.kt` |\n| Tool registry | `app/.../agent/tool/ToolRegistryFactory.kt` |\n| Tool context (services access) | `app/.../agent/tool/ToolContext.kt` |\n| Built-in tools (by domain) | `app/.../agent/tool/builtin/*.kt` |\n| Agent loop / execution | `app/.../agent/engine/AgentEngine.kt` |\n| Permission prompt (Channel-based) | `app/.../agent/permission/PermissionPrompter.kt` |\n| LLM provider (SSE streaming) | `app/.../agent/llm/OpenAiCompatProvider.kt` |\n\n---\n\n## Easy-to-miss points\n\n- **Shared sources are authored in `app/`.** Editing only a file under `shell/src` is usually wrong; it will be overwritten on sync or diverge from host.\n- **Config field names drift.** Editor model, `ApkConfig`, JSON factory, and shell config must stay aligned; Gson silently drops unknown/missing fields. Run `checkConfigFieldDrift`.\n- **Low targetSdk (28) and fork/exec runtimes** constrain \"modernize the shell SDK\" changes.\n- **Incremental export cache** keys must be content-based; mtime and resigned APKs create false hits/misses.\n- **HTML/FRONTEND file access.** Packaged local-file shells must have `allowFileAccess = true` (forced in `buildWebViewBlock` and `ShellWebViewConfig`); do not regress pure file-based HTML loads.\n- **Node native libs.** Exported NODEJS_APP needs `libnode_bridge.so` + `libnode.so` + `libc++_shared.so`; missing any causes `loadNode` / `loadJniBridge` failure at runtime.\n- **16KB page alignment.** `libnode.so` and other large ELF natives must be 16KB-aligned (`ElfAligner16k`) for Android 15+ devices; `node_bridge.cpp` / `node_launcher.c` enable 16KB app-compat before `dlopen`.\n- **Node JNI output bridge.** `NodeJniOutputBridge` is a stable class referenced by native code; keep its `-keep` proguard rule so R8 does not rename `onOutput`.\n- **Crashing FGS when notification channel creation fails.** Always use `SafeNotificationChannels` for channel creation.\n- **Adblock is wired for preview + export.** Do not wipe host filter state during export; the host AdBlocker serves preview and the compiled rule set ships in the APK.\n- **Runtime permissions are feature-driven.** `RuntimePermissionSync` derives the permission list from enabled features; do not revert to a static template.\n- **Splash preview media path.** Preview reads splash media from the host filesystem (`splashMediaPath`); export packages it into assets. Do not hardcode `assets/splash_media.*` as the only source.\n- **Port conflict policy.** Local server runtimes must allocate through `PortManager` and clean up on stop; do not bind ports directly.\n- **Agent tool ↔ service drift.** When a service class API changes, the corresponding Agent tool in `core/agent/tool/builtin/` must be updated in the same PR. A stale tool either fails to compile or silently passes wrong arguments at runtime. Check `ToolRegistryFactory.baseTools()` for the full tool list.\n\n---\n\n## Forbidden / high-risk mistakes\n\n- Second shell template APK\n- Excluding a shell-synced class but leaving imports/constructors in shell-synced sources\n- Re-enabling shell R8 obfuscation / aggressive shrink without testing exported apps\n- Putting host-only tools back into shell sync for convenience\n- OEM push SDKs or unjustified heavy dependencies in shell\n- Feeding signed/renamed APKs into template/modify paths\n- Crashing FGS when notification channel creation fails\n- Committing secrets, keystores, or local machine config\n- Regressing HTML/FRONTEND file access in packaged shells\n- Shipping NODEJS_APP without `libnode_bridge.so` / `libnode.so` / `libc++_shared.so`\n- Skipping 16KB alignment for large ELF natives\n\n---\n\n## Verify commands\n\n```bash\n./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache\n./gradlew :app:compileDebugKotlin -x syncCloneHostDex --no-configuration-cache\n./gradlew :app:checkConfigFieldDrift --no-configuration-cache\npython3 scripts/check_config_field_drift.py\n```\n\nUse these when you change shell membership, export packaging, or config fields. For host-only UI/string work, targeted compile on `:app` is usually enough.\n\nRelated focused tests often worth running after nearby edits: `ApkBuildCacheTest`, `AdBlockerHostRuntimeTest`, `AdBlockExportWiringTest`, `PortManagerTest`, `BuildInputPreflightTest`, `GoBuildEnvironmentTest`, `RuntimePermissionSyncTest`.\n\n---\n\n## Implementation snapshot\n\nLanded:\n\n- Single shell template (`webview_shell.apk`) from `:shell` release, full runtime synced from `app/`\n- Incremental `ApkBuildCache` (`FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`); encrypted builds always full\n- Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions\n- `SafeNotificationChannels` fail-soft path for FGS\n- `PortManager` conflict policies + real stop handlers across Node/PHP/Python/Go/WordPress\n- `LocalDnsBridgeProxy` wiring for local server runtimes (including Node.js)\n- Runtime downloads via `NetworkModule.downloadClient`\n- Adblock preview + export wiring restored\n- HTML/FRONTEND file-access for packaged local shells\n- Node.js export: `libnode_bridge.so` + 16KB-aligned `libnode.so` + `libc++_shared.so`; 16KB app-compat before dlopen; stable `NodeJniOutputBridge` JNI callback\n- Go export: `libgo_exec_loader.so` embedded; in-app build ENOSPC handling + GOTMPDIR relocation\n- Runtime permission sync (feature-driven)\n- Splash preview media path fallback\n- Config field drift detection (`checkConfigFieldDrift`)\n- Module Market: Chrome Web Store live search + GreasyFork browse\n- Code editor find-and-replace\n- Agent tool system: 40+ tools (app lifecycle, ports/engine, hosts/runtime, stats/modifier/import, build env/Play, modules, files) with Channel-based permission prompting, per-section SSE parse resilience, and plan mode\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nInstructions for coding agents working in this repository.\n\n## Code style\n\n- Do what you believe is right. Make the change complete and correct, not the smallest possible diff. If a fix calls for refactoring, renaming, or touching multiple files, do it.\n- Match the patterns and conventions already in the surrounding code.\n- Do not add copyright or license headers unless asked.\n\n## Project layout\n\n| Path | Role |\n|------|------|\n| `app/` | Full builder host: editor UI, export pipeline, runtimes, preview. |\n| `shell/` | Runtime template. Built to `app/src/main/assets/template/webview_shell.apk` via `:shell:assembleRelease` + `:app:syncShellTemplateApk`. |\n| `clone-host/` | Host-side APK clone / identity reshape support library (compiled to a DEX asset). |\n| `modules/` | Module Market catalog (`registry.json` + per-module folders). |\n| `docs/` | VitePress documentation site (guide / developer / extensions, EN + ZH), published to https://shiaho777.github.io/web-to-app/ by `.github/workflows/docs-deploy.yml`. Site URL paths map 1:1 to files under `docs/` (`/zh/...` → `docs/zh/...`). |\n| `scripts/` | Build helpers and gates (`check_config_field_drift.py`). |\n\nRuntime Kotlin is authored under `app/` and synced into `shell` by `syncShellRuntimeSources`. Edit the `app/` source once; do not permanently fork copies under shell.\n\nUser-facing product docs: `README.md`, `.github/docs/README_CN.md`, `.github/CONTRIBUTING.md`, `modules/README.md`. The published documentation site is https://shiaho777.github.io/web-to-app/ (source: `docs/`, deployed by `.github/workflows/docs-deploy.yml`).\n\n## How the main pieces connect\n\n```text\nEditor (Compose screens in app/)\n  ↔ data models (WebApp, configs)\n  ↔ export factory (ApkConfig / ApkConfigJsonFactory)\n  ↔ ApkBuilder / ApkBuildCache  →  signed generated APK\n\napp/ sources\n  → syncShellRuntimeSources  →  shell DEX  →  webview_shell.apk (template)\n\nGenerated APK runtime\n  WebToAppApplication → ShellModeManager → load assets JSON config\n  → WebViewManager / runtime servers (Node/PHP/Python/Go/WordPress)\n```\n\nMental model:\n\n- **Host preview** runs `:app` with all classes on the main classpath.\n- **Generated APK** runs the shell template classes (full runtime synced from `app/`), reading config from assets JSON via `ShellModeManager`.\n- A flag in the editor is useless at export unless it flows through **model → ApkConfig JSON → shell config → runtime code**.\n\n## i18n\n\n- Host UI strings live in `app/src/main/java/com/webtoapp/core/i18n/Strings.kt` (split across `Strings` / `StringsA` … `StringsE`).\n- **All** user-visible strings must be inline `when (Strings.lang)` blocks covering all 10 languages: Chinese, English, Arabic, Portuguese, Spanish, French, German, Russian, Japanese, Korean. `when(lang)` blocks may never use `else ->` — `AppStringsResourceConsistencyTest` and `StringsKtTranslationParityTest` enforce this.\n- **Never** load user-visible text via `context.getString(R.string.*)` / `stringResource(R.string.*)`. `res/values*/` is only maintained for zh/en/ar; the other 7 locales have no `values-*/` directory, so resource lookups silently fall back to the default `values/` (Chinese). Use `Strings.xxx` (or `Strings.funName(arg)` for parameterised strings — see `linuxEnvInstalledToast(name)` for the pattern). A test gates this: `kotlin source never references R string for user-visible text`.\n- `R.string` is reserved for `translatable=\"false\"` non-localised resources only (e.g. `app_name`).\n- Prefer adding properties on the existing split objects; match surrounding style.\n\n## Android and packaging constraints\n\n- Generated apps keep a low `targetSdk` (28) on the shell path because they rely on on-device fork+exec runtimes. Do not raise shell targetSdk casually.\n- The host app targets SDK 35 (antivirus reputation); SELinux W^X therefore blocks host-side exec of downloaded runtimes. `RuntimeExecPolicy` (`core/linux`) gates those previews by probing the installed targetSdk — generated APKs (always 28) pass unconditionally. Node preview (JNI via native libs) is unaffected.\n- Avoid new third-party dependencies unless strongly justified (`app/build.gradle.kts` / `shell/build.gradle.kts`). Prefer platform APIs and existing modules.\n- Notification push channels: Web Notification polyfill, polling, WebSocket, FCM (developer-owned Firebase config). Do not add OEM vendor push SDKs by default.\n- Foreground services and notification helpers must use `SafeNotificationChannels` (or equivalent fail-soft create). Channel creation failures must not crash FGS startup.\n- **One shell template:** `webview_shell.apk` from `:shell` release. Do not introduce a second template APK.\n- Export incremental rebuild lives in `app/.../apkbuilder` (`ApkBuildCache` + `ApkBuilder`):\n  - Modes: `FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`.\n  - Template / entry identities must be **content-stable** (no mtime-based keys).\n  - Encrypted builds always force a full rebuild.\n  - Do not feed signed or renamed APKs back into full `modifyApk` as templates.\n- Port coordination: `PortManager` + `PortConflictMode` (`REASSIGN` / `AUTO_KILL` / `ALERT`) with real stop handlers. Local server runtimes must allocate through PortManager and clean up on stop.\n- Local server / Linux env DNS: fork+exec runtimes (Node / PHP / Python / Go / WordPress / Linux env) should wire through `LocalDnsBridgeProxy` when they need host DNS/proxy bridging.\n- Large runtime downloads use `NetworkModule.downloadClient` (extended timeouts), not the default short-lived client.\n- HTML / FRONTEND packaged shells need file-scheme access via `ShellWebViewConfig` (`allowFileAccess` / local-file detection). Do not regress pure file-based HTML loads.\n- Node.js export must embed `libnode_bridge.so`, `libnode.so` (16KB-aligned), and `libc++_shared.so` as native libs. Go export must embed `libgo_exec_loader.so`.\n- Gradle custom tasks (`syncCloneHostDex`, etc.) must be configuration-cache safe: capture `File`/`Provider` values at configuration time, do not reference `Project`/`android.sdkDirectory` inside task closures.\n\n## Workflow\n\n- Do not commit secrets, `local.properties`, keystores, or IDE/cache junk.\n- Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).\n- When changing export or shell packaging, rebuild the template you touched.\n- When changing config fields, run `checkConfigFieldDrift` to catch model ↔ shell config name drift.\n\n### Delivery (Issue + PR + CI)\n\nDefault target: [shiaho777/web-to-app](https://github.com/shiaho777/web-to-app). Prefer a pull request over direct pushes to `main` when delivering code. Human-facing wording of the same loop lives in [CONTRIBUTING.md](.github/CONTRIBUTING.md); keep those docs in sync when this process changes.\n\n**Language (required):** GitHub **Issues and PRs must be written in English** — titles, bodies, labels text you author, and delivery comments on the Issue/PR. Local chat with the user may be Chinese or any language; do not copy that language into Issue/PR text.\n\nWhen the user asks to deliver a change, run the Issue → branch → PR → CI → merge loop end-to-end. Do not close the Issue until the PR is merged and CI is green.\n\n---\n\n## Shell template and runtime sync\n\n### Dual runtime (preview vs export)\n\n| | Host `:app` | Generated APK |\n|--|-------------|---------------|\n| DEX | All `app/src` classes | Shell sync include−exclude (full runtime set) |\n| Config | Editor / in-memory models | Assets JSON via `ShellModeManager` |\n| Template | Not used | `app/src/main/assets/template/webview_shell.apk` |\n\n**Preview ≠ export** unless both paths stay valid:\n\n1. Host still has the real implementation.\n2. Shell-synced code reads the config field from assets JSON at runtime.\n3. The field name in `ApkConfig` JSON matches the `@SerializedName` in `ShellModeManager` (Gson silently drops mismatches).\n\nMost common failure: preview works; exported APK silently skips the feature because a config field name drifted between the export factory and the shell config class.\n\n### Where each concern is edited\n\n| Concern | Path |\n|---------|------|\n| What enters shell | `shell/build.gradle.kts` → `syncShellRuntimeSources` include/exclude |\n| Shell template build | `:shell:assembleRelease` + `:app:syncShellTemplateApk` |\n| Template output | `app/src/main/assets/template/webview_shell.apk` |\n| Config → shell JSON | `app/.../apkbuilder/ApkConfigJsonFactory.kt` |\n| Shell config types | `app/.../core/shell/ShellModeManager.kt` |\n| Runtime WebView config | `app/.../ui/shell/ShellWebViewConfig.kt` |\n| Config drift gate | `scripts/check_config_field_drift.py` → `:app:checkConfigFieldDrift` |\n| Shell minify policy | `shell/proguard-rules.pro` |\n\n---\n\n## Common change recipes\n\nThese are the default approaches for everyday work. Follow the chain end-to-end; stopping at UI or host-only code is how preview and export diverge.\n\n### 1. Add or change a host UI string\n\n1. Add the property in the correct `Strings*` split with all 10 languages.\n2. Reference it from Compose/UI the same way neighbors do.\n\n### 2. Add an editor setting that must affect the generated APK\n\nTrace and update **all** of:\n\n1. Model (`WebApp` / nested config) and editor UI binding\n2. Export mapping (`ApkBuilder` / `ApkConfig` / `ApkConfigJsonFactory`)\n3. Shell config types (`ShellModeManager` / shell config data classes) if runtime reads them\n4. Runtime use site in shell-synced code\n5. Unit tests for export wiring when flags change\n6. **Coverage test update (REQUIRED).** When adding a new Boolean field to\n   `WebViewConfig`, you MUST add it to `flipAllBooleans()` in\n   `WebViewConfigBooleanCoverageTest.kt`. The test\n   `flipAllBooleans covers every declared Boolean field` uses reflection to\n   verify that every declared Boolean field is listed — if you forget, CI\n   fails with a message naming the missing field. For non-Boolean fields, add\n   a spot-check to `key non-Boolean WebViewConfig fields survive the export\n   round-trip`. This is the safety net that catches \"preview works, export\n   broken\" before it ships.\n\nMissing any step usually yields: editor shows the switch, export ignores it, or export embeds config the runtime never reads.\n\n### 3. Change shell runtime behavior used by every generated app\n\n1. Edit the source under `app/` (shared runtime).\n2. Confirm the file is included by `syncShellRuntimeSources`.\n3. Rebuild shell template if you need to validate packaging.\n4. Keep changes surgical; shell has a low targetSdk and a thin dependency set.\n5. If you touch FGS / notification channel creation, fail soft via `SafeNotificationChannels`.\n\n### 4. Add a host-only feature (editor, market, tooling)\n\n1. Keep implementation under host-only packages (`core/apkbuilder`, host screens, sample/market, `core/host`, …).\n2. Do not pull host-only deps into `shell/build.gradle.kts`.\n\n### 5. Touch APK export or incremental rebuild\n\n1. Prefer `ApkBuildCache` / content hashes over timestamps.\n2. Encrypted builds stay full rebuild.\n3. Do not use signed outputs as templates.\n4. If template bytes change, ensure cache keys invalidate correctly.\n\n### 6. Change notifications / engines / network hardening\n\n1. Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).\n2. Do not add OEM push SDKs by default.\n3. FGS channel create paths must tolerate OEM/channel failures.\n\n### 7. Module Market / `modules/`\n\n1. Follow `modules/README.md` catalog layout (`registry.json` + module folders).\n2. Runtime consumption still goes through the extension/shell paths if it ships inside generated APKs.\n\n### 8. Fix \"works in preview, broken after export\"\n\nChecklist in order:\n\n1. Did shell config JSON actually contain the field at runtime?\n2. Does the field name in `ApkConfig` JSON match the `@SerializedName` in shell config? Run `checkConfigFieldDrift`.\n3. Is the runtime use site shell-synced (not host-only)?\n4. For adblock: confirm `adBlockEnabled` mapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state.\n5. Rebuild template after sync changes (stale template is a frequent miss).\n\n### 9. Local server runtime / download path\n\n1. Allocate ports through `PortManager` with the configured conflict policy; implement real stop handlers.\n2. Wire fork+exec processes into `LocalDnsBridgeProxy` when they need host DNS/proxy env.\n3. Use `NetworkModule.downloadClient` for large dependency / engine / runtime downloads.\n\n### 10. Node.js / Go export\n\n1. Node.js: ensure `injectNodeJsNativeLibs` embeds `libnode_bridge.so` + `libnode.so` (16KB-aligned via `ElfAligner16k`) + `libc++_shared.so`. Node binary resolution prefers `nativeLibraryDir`, falls back to download cache.\n2. Go: ensure `injectGoExecLoaderNativeLib` embeds `libgo_exec_loader.so`.\n3. `NodeService` runs in a dedicated `:nodejs` OS process so V8 lifecycle is isolated from the host.\n\n### 11. Change a feature that has an Agent tool\n\nThe in-app Agent exposes 40+ tools that wrap host service classes. When you change a feature, trace the tool chain:\n\n```text\nLLM response (tool_calls)\n  → AgentEngine.executeToolCall()\n  → PermissionPrompter (write tools ask user; read-only run silently)\n  → Tool.execute(args, ctx)\n  → Service class (ApkBuilder, AppExporter, PortManager, EngineManager, AdBlocker, …)\n  → ToolResult → back to LLM\n```\n\nChecklist when touching a feature that an Agent tool wraps:\n\n1. **API signature drift.** If the service class constructor, method signature, or return type changes, update the tool's `execute()` in `app/.../agent/tool/builtin/`. A stale call compiles only if the old overload still exists; otherwise it's a build break.\n2. **`parametersSchema` ↔ `execute()` alignment.** The JSON schema advertised to the LLM must match what `execute()` actually reads from `args`. Adding a parameter to the service call without exposing it in the schema means the LLM can never pass it.\n3. **`description` accuracy.** The LLM decides *when* and *how* to call a tool solely from `description` + `parametersSchema`. If the feature's behavior changed, update the description text — a misleading description causes silent misuse.\n4. **`isReadOnly()` correctness.** `true` → runs without user confirmation; `false` → triggers `PermissionPrompter` dialog. If a tool gains write side-effects, flip it to `false`.\n5. **Registration.** New tools must be added to `ToolRegistryFactory.baseTools()` (grouped by domain comment). Forgotten registration = tool invisible to the LLM.\n6. **Removed features.** If a feature is deleted, remove or disable its tool. A tool that calls a dead class crashes at runtime inside the agent loop.\n7. **Host-only.** Agent tools live under `core/agent/` and are never shell-synced. Do not add them to `syncShellRuntimeSources`.\n\nKey paths:\n\n| Concern | Path |\n|---------|------|\n| Tool interface | `app/.../agent/tool/Tool.kt` |\n| Tool registry | `app/.../agent/tool/ToolRegistryFactory.kt` |\n| Tool context (services access) | `app/.../agent/tool/ToolContext.kt` |\n| Built-in tools (by domain) | `app/.../agent/tool/builtin/*.kt` |\n| Agent loop / execution | `app/.../agent/engine/AgentEngine.kt` |\n| Permission prompt (Channel-based) | `app/.../agent/permission/PermissionPrompter.kt` |\n| LLM provider (SSE streaming) | `app/.../agent/llm/OpenAiCompatProvider.kt` |\n\n---\n\n## Easy-to-miss points\n\n- **Shared sources are authored in `app/`.** Editing only a file under `shell/src` is usually wrong; it will be overwritten on sync or diverge from host.\n- **Config field names drift.** Editor model, `ApkConfig`, JSON factory, and shell config must stay aligned; Gson silently drops unknown/missing fields. Run `checkConfigFieldDrift`.\n- **Low targetSdk (28) and fork/exec runtimes** constrain \"modernize the shell SDK\" changes.\n- **Incremental export cache** keys must be content-based; mtime and resigned APKs create false hits/misses.\n- **HTML/FRONTEND file access.** Packaged local-file shells must have `allowFileAccess = true` (forced in `buildWebViewBlock` and `ShellWebViewConfig`); do not regress pure file-based HTML loads.\n- **Node native libs.** Exported NODEJS_APP needs `libnode_bridge.so` + `libnode.so` + `libc++_shared.so`; missing any causes `loadNode` / `loadJniBridge` failure at runtime.\n- **16KB page alignment.** `libnode.so` and other large ELF natives must be 16KB-aligned (`ElfAligner16k`) for Android 15+ devices; `node_bridge.cpp` / `node_launcher.c` enable 16KB app-compat before `dlopen`.\n- **Node JNI output bridge.** `NodeJniOutputBridge` is a stable class referenced by native code; keep its `-keep` proguard rule so R8 does not rename `onOutput`.\n- **Crashing FGS when notification channel creation fails.** Always use `SafeNotificationChannels` for channel creation.\n- **Adblock is wired for preview + export.** Do not wipe host filter state during export; the host AdBlocker serves preview and the compiled rule set ships in the APK.\n- **Runtime permissions are feature-driven.** `RuntimePermissionSync` derives the permission list from enabled features; do not revert to a static template.\n- **Splash preview media path.** Preview reads splash media from the host filesystem (`splashMediaPath`); export packages it into assets. Do not hardcode `assets/splash_media.*` as the only source.\n- **Port conflict policy.** Local server runtimes must allocate through `PortManager` and clean up on stop; do not bind ports directly.\n- **Agent tool ↔ service drift.** When a service class API changes, the corresponding Agent tool in `core/agent/tool/builtin/` must be updated in the same PR. A stale tool either fails to compile or silently passes wrong arguments at runtime. Check `ToolRegistryFactory.baseTools()` for the full tool list.\n\n---\n\n## Forbidden / high-risk mistakes\n\n- Second shell template APK\n- Excluding a shell-synced class but leaving imports/constructors in shell-synced sources\n- Re-enabling shell R8 obfuscation / aggressive shrink without testing exported apps\n- Putting host-only tools back into shell sync for convenience\n- OEM push SDKs or unjustified heavy dependencies in shell\n- Feeding signed/renamed APKs into template/modify paths\n- Crashing FGS when notification channel creation fails\n- Committing secrets, keystores, or local machine config\n- Regressing HTML/FRONTEND file access in packaged shells\n- Shipping NODEJS_APP without `libnode_bridge.so` / `libnode.so` / `libc++_shared.so`\n- Skipping 16KB alignment for large ELF natives\n\n---\n\n## Verify commands\n\n```bash\n./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache\n./gradlew :app:compileDebugKotlin -x syncCloneHostDex --no-configuration-cache\n./gradlew :app:checkConfigFieldDrift --no-configuration-cache\npython3 scripts/check_config_field_drift.py\n```\n\nUse these when you change shell membership, export packaging, or config fields. For host-only UI/string work, targeted compile on `:app` is usually enough.\n\nRelated focused tests often worth running after nearby edits: `ApkBuildCacheTest`, `AdBlockerHostRuntimeTest`, `AdBlockExportWiringTest`, `PortManagerTest`, `BuildInputPreflightTest`, `GoBuildEnvironmentTest`, `RuntimePermissionSyncTest`.\n\n---\n\n## Implementation snapshot\n\nLanded:\n\n- Single shell template (`webview_shell.apk`) from `:shell` release, full runtime synced from `app/`\n- Incremental `ApkBuildCache` (`FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`); encrypted builds always full\n- Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions\n- `SafeNotificationChannels` fail-soft path for FGS\n- `PortManager` conflict policies + real stop handlers across Node/PHP/Python/Go/WordPress\n- `LocalDnsBridgeProxy` wiring for local server runtimes (including Node.js)\n- Runtime downloads via `NetworkModule.downloadClient`\n- Adblock preview + export wiring restored\n- HTML/FRONTEND file-access for packaged local shells\n- Node.js export: `libnode_bridge.so` + 16KB-aligned `libnode.so` + `libc++_shared.so`; 16KB app-compat before dlopen; stable `NodeJniOutputBridge` JNI callback\n- Go export: `libgo_exec_loader.so` embedded; in-app build ENOSPC handling + GOTMPDIR relocation\n- Runtime permission sync (feature-driven)\n- Splash preview media path fallback\n- Config field drift detection (`checkConfigFieldDrift`)\n- Module Market: Chrome Web Store live search + GreasyFork browse\n- Code editor find-and-replace\n- Agent tool system: 40+ tools (app lifecycle, ports/engine, hosts/runtime, stats/modifier/import, build env/Play, modules, files) with Channel-based permission prompting, per-section SSE parse resilience, and plan mode\n","category":"root","tokens":5150}]}