drukscode-v2/AGENTS.md
OpenCode AI e9899d2e6d
Some checks failed
Android CI Build / check (push) Has been cancelled
Android CI Build / package-apks (push) Has been cancelled
Docs Deploy / build (push) Has been cancelled
Module Market Check / Validate modules/ (push) Has been cancelled
Module Market Publish / Generate submissions.json (push) Has been cancelled
Docs Deploy / deploy (push) Has been cancelled
Initial DruksCode V2: fresh fork of web-to-app (f3b488e) with DruksCode identity
- Package rename com.webtoapp -> br.com.drukstech.codeapp
- Product rename WebToApp/web-to-app -> DruksCode/drukscode
- Design system Wta* -> Dkc*
- applicationId br.com.drukstech.codeapp (.play for gplay flavor)
- rootProject.name DruksCode
- Fresh git history (upstream kept as remote for sync)
2026-09-12 02:42:02 +02:00

371 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md
Instructions for coding agents working in this repository.
## Code style
- 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.
- Match the patterns and conventions already in the surrounding code.
- Do not add copyright or license headers unless asked.
## Project layout
| Path | Role |
|------|------|
| `app/` | Full builder host: editor UI, export pipeline, runtimes, preview. |
| `shell/` | Runtime template. Built to `app/src/main/assets/template/webview_shell.apk` via `:shell:assembleRelease` + `:app:syncShellTemplateApk`. |
| `clone-host/` | Host-side APK clone / identity reshape support library. Its DEX asset generation (`syncCloneHostDex`) is deliberately disabled (`enabled = false`, AV false-positive mitigation, e0d2d4d6) — `AppCloner` runs fail-soft without the asset. |
| `modules/` | Module Market catalog (`registry.json` + per-module folders). |
| `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/...`). |
| `scripts/` | Build helpers and gates (`check_config_field_drift.py`). |
Runtime Kotlin is authored under `app/` and synced into `shell` by `syncShellRuntimeSources`. Edit the `app/` source once; do not permanently fork copies under shell.
User-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`).
## How the main pieces connect
```text
Editor (Compose screens in app/)
↔ data models (WebApp, configs)
↔ export factory (ApkConfig / ApkConfigJsonFactory)
↔ ApkBuilder / ApkBuildCache → signed generated APK
app/ sources
→ syncShellRuntimeSources → shell DEX → webview_shell.apk (template)
Generated APK runtime
DruksCodeApplication → ShellModeManager → load assets JSON config
→ WebViewManager / runtime servers (Node/PHP/Python/Go/WordPress)
```
Mental model:
- **Host preview** runs `:app` with all classes on the main classpath.
- **Generated APK** runs the shell template classes (full runtime synced from `app/`), reading config from assets JSON via `ShellModeManager`.
- A flag in the editor is useless at export unless it flows through **model → ApkConfig JSON → shell config → runtime code**.
## i18n
- Host UI strings live in `app/src/main/java/br/com/drukstech/codeapp/core/i18n/Strings.kt` (split across `Strings` / `StringsA` … `StringsE`).
- **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.
- **Never** load user-visible text via `context.getString(R.string.*)` / `stringResource(R.string.*)`. `res/values/strings.xml` holds only `translatable="false"` resources (e.g. `app_name`) and no locale `values-*/` directories exist, so a localized resource string could never cover the 10 languages and would silently fall back to the default `values/` (Chinese). Use `Strings.xxx` (or `Strings.funName(arg)` for parameterised strings — see `linuxEnvInstalledToast(name)` for the pattern). Tests gate this: `kotlin source never references R string for user-visible text`, plus `values strings xml only holds non-localised resources` and `no locale values dirs or grouped app strings files exist` which block resurrecting resource-based strings.
- `R.string` is reserved for `translatable="false"` non-localised resources only (e.g. `app_name`).
- Prefer adding properties on the existing split objects; match surrounding style.
## Android and packaging constraints
- 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.
- 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.
- Launch runtime / toolchain processes through `HostProcessLauncher` (`core/linux`): plain `ProcessBuilder` fork+exec where the platform allows it (generated APKs, targetSdk 28), the user-mode static exec loader (`StaticExecProcess`, memfd-based) on W^X hosts (targetSdk ≥ 29), or a degraded error when no static exec bridge is available. A raw `Runtime.exec` on a downloaded binary crashes the host app on W^X devices (#795).
- Avoid new third-party dependencies unless strongly justified (`app/build.gradle.kts` / `shell/build.gradle.kts`). Prefer platform APIs and existing modules.
- Notification push channels: Web Notification polyfill, polling, WebSocket, FCM (developer-owned Firebase config). Do not add OEM vendor push SDKs by default.
- Foreground services and notification helpers must use `SafeNotificationChannels` (or equivalent fail-soft create). Channel creation failures must not crash FGS startup.
- **One shell template:** `webview_shell.apk` from `:shell` release. Do not introduce a second template APK.
- Export incremental rebuild lives in `app/.../apkbuilder` (`ApkBuildCache` + `ApkBuilder`):
- Modes: `FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`.
- Template / entry identities must be **content-stable** (no mtime-based keys).
- Encrypted builds always force a full rebuild.
- Do not feed signed or renamed APKs back into full `modifyApk` as templates.
- Port coordination: `PortManager` + `PortConflictMode` (`REASSIGN` / `AUTO_KILL` / `ALERT`) with real stop handlers. Local server runtimes must allocate through PortManager and clean up on stop.
- 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.
- Large runtime downloads use `NetworkModule.downloadClient` (extended timeouts), not the default short-lived client.
- HTML / FRONTEND packaged shells need file-scheme access via `ShellWebViewConfig` (`allowFileAccess` / local-file detection). Do not regress pure file-based HTML loads.
- 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`.
- 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.
## Workflow
- Do not commit secrets, `local.properties`, keystores, or IDE/cache junk.
- Do not create commits, push, open PRs, or file Issues unless the user asks to deliver / ship / push / open a PR (or equivalent).
- When changing export or shell packaging, rebuild the template you touched.
- When changing config fields, run `checkConfigFieldDrift` to catch model ↔ shell config name drift.
### Delivery (Issue + PR + CI)
Default target: [shiaho777/drukscode](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.
**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.
When 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.
---
## Shell template and runtime sync
### Dual runtime (preview vs export)
| | Host `:app` | Generated APK |
|--|-------------|---------------|
| DEX | All `app/src` classes | Shell sync include−exclude (full runtime set) |
| Config | Editor / in-memory models | Assets JSON via `ShellModeManager` |
| Template | Not used | `app/src/main/assets/template/webview_shell.apk` |
**Preview ≠ export** unless both paths stay valid:
1. Host still has the real implementation.
2. Shell-synced code reads the config field from assets JSON at runtime.
3. The field name in `ApkConfig` JSON matches the `@SerializedName` in `ShellModeManager` (Gson silently drops mismatches).
Most 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.
### Where each concern is edited
| Concern | Path |
|---------|------|
| What enters shell | `shell/build.gradle.kts` → `syncShellRuntimeSources` include/exclude |
| Shell template build | `:shell:assembleRelease` + `:app:syncShellTemplateApk` |
| Template output | `app/src/main/assets/template/webview_shell.apk` |
| Config → shell JSON | `app/.../apkbuilder/ApkConfigJsonFactory.kt` |
| Shell config types | `app/.../core/shell/ShellModeManager.kt` |
| Runtime WebView config | `app/.../ui/shell/ShellWebViewConfig.kt` |
| Config drift gate | `scripts/check_config_field_drift.py` → `:app:checkConfigFieldDrift` |
| Shell minify policy | `shell/proguard-rules.pro` |
---
## Common change recipes
These 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.
### 1. Add or change a host UI string
1. Add the property in the correct `Strings*` split with all 10 languages.
2. Reference it from Compose/UI the same way neighbors do.
### 2. Add an editor setting that must affect the generated APK
Trace and update **all** of:
1. Model (`WebApp` / nested config) and editor UI binding
2. Export mapping (`ApkBuilder` / `ApkConfig` / `ApkConfigJsonFactory`)
3. Shell config types (`ShellModeManager` / shell config data classes) if runtime reads them
4. Runtime use site in shell-synced code
5. Unit tests for export wiring when flags change
6. **Coverage test update (REQUIRED).** When adding a new Boolean field to
`WebViewConfig`, you MUST add it to `flipAllBooleans()` in
`WebViewConfigBooleanCoverageTest.kt`. The test
`flipAllBooleans covers every declared Boolean field` uses reflection to
verify that every declared Boolean field is listed — if you forget, CI
fails with a message naming the missing field. For non-Boolean fields, add
a spot-check to `key non-Boolean WebViewConfig fields survive the export
round-trip`. This is the safety net that catches "preview works, export
broken" before it ships.
7. **Shell UI parity (REQUIRED for per-type player screens).** Gallery / media /
splash-style app types render through DIFFERENT composables in preview
(`ui/gallery/`, `ui/media/`) vs generated APKs (`ui/shell/`). A flag that flows
end-to-end still does nothing if the shell screen never reads it (gallery
thumbnail bar shipped config + assets correctly and still rendered nothing,
#781). `ShellUiParityTest.kt` enforces this: every model field read by host
runtime UI must also be read by shell runtime UI. If you add a field and read
it in a host player, the test fails naming the field until you port the
behavior to shell or add an explicit `allow` entry with a reason. When adding
a new per-type player pair, extend its `pairs` list the same way.
Missing any step usually yields: editor shows the switch, export ignores it, or export embeds config the runtime never reads.
### 3. Change shell runtime behavior used by every generated app
1. Edit the source under `app/` (shared runtime).
2. Confirm the file is included by `syncShellRuntimeSources`.
3. Rebuild shell template if you need to validate packaging.
4. Keep changes surgical; shell has a low targetSdk and a thin dependency set.
5. If you touch FGS / notification channel creation, fail soft via `SafeNotificationChannels`.
### 4. Add a host-only feature (editor, market, tooling)
1. Keep implementation under host-only packages (`core/apkbuilder`, host screens, sample/market, `core/host`, …).
2. Do not pull host-only deps into `shell/build.gradle.kts`.
### 5. Touch APK export or incremental rebuild
1. Prefer `ApkBuildCache` / content hashes over timestamps.
2. Encrypted builds stay full rebuild.
3. Do not use signed outputs as templates.
4. If template bytes change, ensure cache keys invalidate correctly.
### 6. Change notifications / engines / network hardening
1. Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).
2. Do not add OEM push SDKs by default.
3. FGS channel create paths must tolerate OEM/channel failures.
### 7. Module Market / `modules/`
1. Follow `modules/README.md` catalog layout (`registry.json` + module folders).
2. Runtime consumption still goes through the extension/shell paths if it ships inside generated APKs.
### 8. Fix "works in preview, broken after export"
Checklist in order:
1. Did shell config JSON actually contain the field at runtime?
2. Does the field name in `ApkConfig` JSON match the `@SerializedName` in shell config? Run `checkConfigFieldDrift`.
3. Is the runtime use site shell-synced (not host-only)?
4. For adblock: confirm `adBlockEnabled` mapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state.
5. Rebuild template after sync changes (stale template is a frequent miss).
### 9. Local server runtime / download path
1. Allocate ports through `PortManager` with the configured conflict policy; implement real stop handlers.
2. Wire fork+exec processes into `LocalDnsBridgeProxy` when they need host DNS/proxy env.
3. Use `NetworkModule.downloadClient` for large dependency / engine / runtime downloads.
4. Launch processes through `HostProcessLauncher` (`core/linux`) so W^X hosts (targetSdk ≥ 29) degrade to the user-mode static exec loader instead of crashing.
### 10. Node.js / Go export
1. 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.
2. Go: ensure `injectGoExecLoaderNativeLib` embeds `libgo_exec_loader.so`.
3. Go host-side builds never run the multi-process `go build` driver: `GoDirectBuilder` replays `compile`/`asm`/`link` as single-shot processes through `HostProcessLauncher` (user-mode static exec loader on W^X hosts), with a content-keyed persistent package archive cache (#796).
4. `NodeService` runs in a dedicated `:nodejs` OS process so V8 lifecycle is isolated from the host.
### 11. Change a feature that has an Agent tool
The in-app Agent exposes up to 57 tools — 52 base + 2 plan-mode + 3 imagery (imagery only load with an image-capable model; see `ToolRegistryFactory.build()`) — that wrap host service classes. When you change a feature, trace the tool chain:
```text
LLM response (tool_calls)
→ AgentEngine.executeToolCall()
→ PermissionPrompter (write tools ask user; read-only run silently)
→ Tool.execute(args, ctx)
→ Service class (ApkBuilder, AppExporter, PortManager, EngineManager, AdBlocker, …)
→ ToolResult → back to LLM
```
Checklist when touching a feature that an Agent tool wraps:
1. **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.
2. **`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.
3. **`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.
4. **`isReadOnly()` correctness.** `true` → runs without user confirmation; `false` → triggers `PermissionPrompter` dialog. If a tool gains write side-effects, flip it to `false`.
5. **Registration.** New tools must be added to `ToolRegistryFactory.baseTools()` (grouped by domain comment). Forgotten registration = tool invisible to the LLM.
6. **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.
7. **Host-only.** Agent tools live under `core/agent/` and are never shell-synced. Do not add them to `syncShellRuntimeSources`.
Key paths:
| Concern | Path |
|---------|------|
| Tool interface | `app/.../agent/tool/Tool.kt` |
| Tool registry | `app/.../agent/tool/ToolRegistryFactory.kt` |
| Tool context (services access) | `app/.../agent/tool/ToolContext.kt` |
| Built-in tools (by domain) | `app/.../agent/tool/builtin/*.kt` |
| Agent loop / execution | `app/.../agent/engine/AgentEngine.kt` |
| Permission prompt (Channel-based) | `app/.../agent/permission/PermissionPrompter.kt` |
| LLM provider (SSE streaming) | `app/.../agent/llm/OpenAiCompatProvider.kt` |
### 12. Change editor config-card UI (Compose)
The editor screens are built from `DkcSettingCard`s that share one visual grammar. The activation-card series (issue #567: PRs #568 → #571 → #573 → #574) took four attempts because each round invented layout instead of copying the neighbours, and each round was only compile-checked. A short prompt like "改一下 XX 卡的 UI" still means: follow these rules.
**Copy, don't invent.** Before writing any card, open the reference implementation on the same screen and mirror it element by element:
| You are writing… | Copy from (`CreateAppWebViewCards.kt`) |
|------------------|----------------------------------------|
| Toggle-headed feature card | `FullscreenModeCard` |
| Collapsible card with `DkcChoiceRow` header | `BrowserAdvancedConfigCard` |
| Radio group (single-choice) | PWA offline-strategy selector in `sectionOfflinePerformance` |
| Sub-toggles / fields inside an expanded zone | `staticAssetPack` block, proxy block |
| Nested conditional content | proxy MANUAL/PAC `AnimatedVisibility` swap |
Inputs and dialogs follow the same grammar: text inputs are `PremiumTextField`, list-manager dialogs are `Dialog` + `DkcCard` + embedded `TopAppBar` (see `AdBlockSubscriptionSelectorDialog`), and `ActivationCodeCard`'s offline-policy rows are another canonical radio reference.
Hard rules learned the hard way:
1. **Two sanctioned card shapes.** Toggle header: `DkcSettingCard { DkcToggleRow(header); AnimatedVisibility(enabled, CardExpandTransition/CardCollapseTransition) { Column { DkcSectionDivider(); full-bleed rows separated by more dividers } } }`. Collapse header: `DkcSettingCard { Column { DkcChoiceRow(header); AnimatedVisibility(expanded) { Column(padding(horizontal = DkcSpacing.RowHorizontal, vertical = DkcSpacing.ContentGap), spacedBy(DkcSpacing.ContentGap)) { … } } } }`.
2. **Alignment grid.** `DkcToggleRow` / `DkcChoiceRow` are full-bleed rows carrying their own 16dp padding. Anything that is *not* a row — text fields, radios, group labels, buttons, notes — must sit inside a `Column(padding(horizontal = DkcSpacing.RowHorizontal, vertical = DkcSpacing.ContentGap))` zone. Sub-toggles inside a zone double-indent (16dp zone + 16dp row); that matches the reference, do not "fix" it. Group labels are `Text(style = labelMedium, color = primary)`, not `DkcSection`.
3. **Radio groups** are exactly: `Text(labelMedium, primary)` label, then `Row(fillMaxWidth().clip(MaterialTheme.shapes.small).clickable { … }.padding(vertical = 4.dp)) { RadioButton; Spacer(4.dp); Text(bodySmall) }`. Never wrap a radio group in `DkcSection`, never attach per-option prose.
4. **Card headers carry title + icon only.** No subtitle on the header row, no free-floating description text next to it. Subtitles belong on child rows that genuinely need one (e.g. 显示状态栏).
5. **Expansion state ≠ feature state.** Never bind a section's `isExpanded` / `AnimatedVisibility(visible=…)` to the feature's `enabled` flag — expanding a panel must never switch the feature on. Section collapse state is its own `remember { mutableStateOf }`.
6. **Conditional sub-blocks** (mode swaps, dependent fields) use `AnimatedVisibility` with `CardExpandTransition` / `CardCollapseTransition`, never bare `if` inside the card body.
7. **Compile ≠ verified.** After any card UI change, build + install on the emulator and check the rendered card: `uiautomator dump` element bounds, compare left edges / row heights of the changed card against its neighbours on the same screen (they must share the same content columns), plus a screenshot pass. Content a few dp off the grid is invisible in code review and obvious on screen.
8. **If a UI rework PR gets "this doesn't match the other cards" feedback**, the fix is realignment to these patterns, not further invention.
---
## Easy-to-miss points
- **Keyboard avoidance below API 30 requires the classic window path.** Android 10 and lower have no native IME-inset dispatch: an edge-to-edge window (`decorFitsSystemWindows = false`) is never resized for the keyboard and reports zero IME insets, so no `softInputMode` value helps there. `WindowHelper.applyImmersiveFullscreen` therefore keeps the decor fitting system windows on the RESIZE keyboard path below API 30 (system `SOFT_INPUT_ADJUST_RESIZE` works) and degrades TRANSPARENT/IMAGE status-bar styles to solid colors on that path. Do not re-enable edge-to-edge unconditionally for those devices (#613; #634 flipped only the softInputMode bits and fixed nothing — its Robolectric test passed because it never asserted the layout flags).
- **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.
- **Config field names drift.** Editor model, `ApkConfig`, JSON factory, and shell config must stay aligned; Gson silently drops unknown/missing fields. Run `checkConfigFieldDrift`.
- **Low targetSdk (28) and fork/exec runtimes** constrain "modernize the shell SDK" changes.
- **Incremental export cache** keys must be content-based; mtime and resigned APKs create false hits/misses.
- **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.
- **Node native libs.** Exported NODEJS_APP needs `libnode_bridge.so` + `libnode.so` + `libc++_shared.so`; missing any causes `loadNode` / `loadJniBridge` failure at runtime.
- **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`.
- **Node JNI output bridge.** `NodeJniOutputBridge` is a stable class referenced by native code; keep its `-keep` proguard rule so R8 does not rename `onOutput`.
- **Crashing FGS when notification channel creation fails.** Always use `SafeNotificationChannels` for channel creation.
- **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.
- **Runtime permissions are feature-driven.** `RuntimePermissionSync` derives the permission list from enabled features; do not revert to a static template.
- **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.
- **Host player UI ≠ shell player UI.** Preview players (`ui/gallery/`, `ui/media/`) and generated-APK players (`ui/shell/`) are separate composables sharing only the config. Port every visible behavior across (thumbnail bar, background, keep-screen-on, orientation) and let `ShellUiParityTest` verify the flags; config flowing end-to-end is necessary but not sufficient.
- **Port conflict policy.** Local server runtimes must allocate through `PortManager` and clean up on stop; do not bind ports directly.
- **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.
- **Editor card UI grammar.** Config-screen cards share one layout grammar (recipe 12): rows are full-bleed, non-row content sits in 16dp-padded zones, expansion never toggles the feature, and card UI is verified on the emulator — not just compiled.
---
## Forbidden / high-risk mistakes
- Second shell template APK
- Excluding a shell-synced class but leaving imports/constructors in shell-synced sources
- Re-enabling shell R8 obfuscation / aggressive shrink without testing exported apps
- Putting host-only tools back into shell sync for convenience
- OEM push SDKs or unjustified heavy dependencies in shell
- Feeding signed/renamed APKs into template/modify paths
- Crashing FGS when notification channel creation fails
- Committing secrets, keystores, or local machine config
- Regressing HTML/FRONTEND file access in packaged shells
- Shipping a host player-screen feature without its shell counterpart (preview-only UI)
- Shipping NODEJS_APP without `libnode_bridge.so` / `libnode.so` / `libc++_shared.so`
- Skipping 16KB alignment for large ELF natives
- Inventing editor card layout/spacing/animations instead of copying the neighbouring cards' patterns, or shipping card UI verified only by compilation
---
## Verify commands
```bash
./gradlew :shell:assembleRelease :app:syncShellTemplateApk --no-configuration-cache
./gradlew :app:compileStandardDebugKotlin -x syncCloneHostDex --no-configuration-cache
./gradlew :app:checkConfigFieldDrift --no-configuration-cache
python3 scripts/check_config_field_drift.py
```
Use these when you change shell membership, export packaging, or config fields. For host-only UI/string work, targeted compile on `:app` is usually enough.
CI note: the PR `check` job compiles only `:shell:compileDebugKotlin` (debug variant) and skips template sync (`-PskipShellTemplateSync=true`). The shell **release** variant (R8 / proguard) and the template pipeline are exercised only by the manual `workflow_dispatch` packaging job — after touching `shell/proguard-rules.pro` or shell packaging, run the first command locally before pushing.
Related focused tests often worth running after nearby edits: `ApkBuildCacheTest`, `AdBlockerHostRuntimeTest`, `AdBlockExportWiringTest`, `PortManagerTest`, `BuildInputPreflightTest`, `GoBuildEnvironmentTest`, `RuntimePermissionSyncTest`.
---
## Implementation snapshot
Landed:
- Single shell template (`webview_shell.apk`) from `:shell` release, full runtime synced from `app/`
- Incremental `ApkBuildCache` (`FULL` / `CONTENT_OVERLAY` / `REUSE_UNSIGNED`); encrypted builds always full
- Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions
- `SafeNotificationChannels` fail-soft path for FGS
- `PortManager` conflict policies + real stop handlers across Node/PHP/Python/Go/WordPress
- `LocalDnsBridgeProxy` wiring for local server runtimes (including Node.js)
- Runtime downloads via `NetworkModule.downloadClient`
- Adblock preview + export wiring restored
- HTML/FRONTEND file-access for packaged local shells
- Node.js export: `libnode_bridge.so` + 16KB-aligned `libnode.so` + `libc++_shared.so`; 16KB app-compat before dlopen; stable `NodeJniOutputBridge` JNI callback
- Go export: `libgo_exec_loader.so` embedded; in-app build ENOSPC handling + GOTMPDIR relocation
- Go host-side builds on W^X hosts: `GoDirectBuilder` replays `compile`/`asm`/`link` as single-shot processes through `HostProcessLauncher` (user-mode static exec loader), with a content-keyed persistent package archive cache (#796)
- Multi-web: gallery/media site sources embedded into the APK and resolved at runtime; nested site sources degrade to a URL instead of aborting the build (#798, #792)
- Gallery playback: shuffleOnLoop, rememberPosition, overview grid, thumbnail bar, pinch-to-zoom viewer, with host/shell parity enforced by `ShellUiParityTest` (#781, #786, #801)
- Untrusted bitmap decodes bounded (oversized-image crash fix) (#788)
- Backup/restore coverage aligned with the current storage layout; restarts only when local files change, with throttled progress callbacks (#790, #794)
- NativeBridge enabled by default for newly created apps (#805)
- Runtime permission sync (feature-driven)
- Splash preview media path fallback
- Config field drift detection (`checkConfigFieldDrift`)
- Module Market: Chrome Web Store live search + GreasyFork browse
- Code editor find-and-replace
- Security hardening sweep: TLS-fingerprint bridge validates upstream certs (system + custom CAs + hostname) and its local CA is signature-verified with no error-type fallback; JS bridges are caller/origin/scheme-gated (NativeBridge CORS bypass, GM bridge, MV3 `ChromeHostPermissions`); MITM proxy host-allowlisted and CA key wrapped at rest; zip extraction routed through `util/SafeZip`; concurrent exports serialized per package with per-package work dirs; multi-web server-runtime site sources degrade to URL; PHP/Python/WP embed failures fail the build; error pages and `TranslateBridge` callbacks JSON-escaped; keystore password sidecars excluded from backups
- Agent tool system: up to 57 tools — 52 base + 2 plan-mode + 3 imagery (image-capable models only) — covering app lifecycle, config templates, ports/engine, hosts/runtime, stats/modifier/import, build env/Play, modules, files, and imagery, with Channel-based permission prompting, per-section SSE parse resilience, and plan mode; runtime/build-env tools surface `localExecAllowed` so the LLM knows targetSdk>=29 hosts cannot exec app-storage binaries