- 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)
30 KiB
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
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
:appwith all classes on the main classpath. - Generated APK runs the shell template classes (full runtime synced from
app/), reading config from assets JSON viaShellModeManager. - 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 acrossStrings/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 useelse ->—AppStringsResourceConsistencyTestandStringsKtTranslationParityTestenforce this. - Never load user-visible text via
context.getString(R.string.*)/stringResource(R.string.*).res/values/strings.xmlholds onlytranslatable="false"resources (e.g.app_name) and no localevalues-*/directories exist, so a localized resource string could never cover the 10 languages and would silently fall back to the defaultvalues/(Chinese). UseStrings.xxx(orStrings.funName(arg)for parameterised strings — seelinuxEnvInstalledToast(name)for the pattern). Tests gate this:kotlin source never references R string for user-visible text, plusvalues strings xml only holds non-localised resourcesandno locale values dirs or grouped app strings files existwhich block resurrecting resource-based strings. R.stringis reserved fortranslatable="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): plainProcessBuilderfork+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 rawRuntime.execon 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.apkfrom:shellrelease. 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
modifyApkas templates.
- Modes:
- 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
LocalDnsBridgeProxywhen 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), andlibc++_shared.soas native libs. Go export must embedlibgo_exec_loader.so. - Gradle custom tasks (
syncCloneHostDex, etc.) must be configuration-cache safe: captureFile/Providervalues at configuration time, do not referenceProject/android.sdkDirectoryinside 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
checkConfigFieldDriftto catch model ↔ shell config name drift.
Delivery (Issue + PR + CI)
Default target: shiaho777/drukscode. Prefer a pull request over direct pushes to main when delivering code. Human-facing wording of the same loop lives in 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:
- Host still has the real implementation.
- Shell-synced code reads the config field from assets JSON at runtime.
- The field name in
ApkConfigJSON matches the@SerializedNameinShellModeManager(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
- Add the property in the correct
Strings*split with all 10 languages. - 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:
- Model (
WebApp/ nested config) and editor UI binding - Export mapping (
ApkBuilder/ApkConfig/ApkConfigJsonFactory) - Shell config types (
ShellModeManager/ shell config data classes) if runtime reads them - Runtime use site in shell-synced code
- Unit tests for export wiring when flags change
- Coverage test update (REQUIRED). When adding a new Boolean field to
WebViewConfig, you MUST add it toflipAllBooleans()inWebViewConfigBooleanCoverageTest.kt. The testflipAllBooleans covers every declared Boolean fielduses 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 tokey non-Boolean WebViewConfig fields survive the export round-trip. This is the safety net that catches "preview works, export broken" before it ships. - 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.ktenforces 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 explicitallowentry with a reason. When adding a new per-type player pair, extend itspairslist 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
- Edit the source under
app/(shared runtime). - Confirm the file is included by
syncShellRuntimeSources. - Rebuild shell template if you need to validate packaging.
- Keep changes surgical; shell has a low targetSdk and a thin dependency set.
- If you touch FGS / notification channel creation, fail soft via
SafeNotificationChannels.
4. Add a host-only feature (editor, market, tooling)
- Keep implementation under host-only packages (
core/apkbuilder, host screens, sample/market,core/host, …). - Do not pull host-only deps into
shell/build.gradle.kts.
5. Touch APK export or incremental rebuild
- Prefer
ApkBuildCache/ content hashes over timestamps. - Encrypted builds stay full rebuild.
- Do not use signed outputs as templates.
- If template bytes change, ensure cache keys invalidate correctly.
6. Change notifications / engines / network hardening
- Prefer existing channel abstractions (polyfill / polling / WebSocket / FCM).
- Do not add OEM push SDKs by default.
- FGS channel create paths must tolerate OEM/channel failures.
7. Module Market / modules/
- Follow
modules/README.mdcatalog layout (registry.json+ module folders). - 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:
- Did shell config JSON actually contain the field at runtime?
- Does the field name in
ApkConfigJSON match the@SerializedNamein shell config? RuncheckConfigFieldDrift. - Is the runtime use site shell-synced (not host-only)?
- For adblock: confirm
adBlockEnabledmapping, host filter rebuild from cached subscriptions, and export rule compile without wiping host state. - Rebuild template after sync changes (stale template is a frequent miss).
9. Local server runtime / download path
- Allocate ports through
PortManagerwith the configured conflict policy; implement real stop handlers. - Wire fork+exec processes into
LocalDnsBridgeProxywhen they need host DNS/proxy env. - Use
NetworkModule.downloadClientfor large dependency / engine / runtime downloads. - 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
- Node.js: ensure
injectNodeJsNativeLibsembedslibnode_bridge.so+libnode.so(16KB-aligned viaElfAligner16k) +libc++_shared.so. Node binary resolution prefersnativeLibraryDir, falls back to download cache. - Go: ensure
injectGoExecLoaderNativeLibembedslibgo_exec_loader.so. - Go host-side builds never run the multi-process
go builddriver:GoDirectBuilderreplayscompile/asm/linkas single-shot processes throughHostProcessLauncher(user-mode static exec loader on W^X hosts), with a content-keyed persistent package archive cache (#796). NodeServiceruns in a dedicated:nodejsOS 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:
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:
- API signature drift. If the service class constructor, method signature, or return type changes, update the tool's
execute()inapp/.../agent/tool/builtin/. A stale call compiles only if the old overload still exists; otherwise it's a build break. parametersSchema↔execute()alignment. The JSON schema advertised to the LLM must match whatexecute()actually reads fromargs. Adding a parameter to the service call without exposing it in the schema means the LLM can never pass it.descriptionaccuracy. The LLM decides when and how to call a tool solely fromdescription+parametersSchema. If the feature's behavior changed, update the description text — a misleading description causes silent misuse.isReadOnly()correctness.true→ runs without user confirmation;false→ triggersPermissionPrompterdialog. If a tool gains write side-effects, flip it tofalse.- Registration. New tools must be added to
ToolRegistryFactory.baseTools()(grouped by domain comment). Forgotten registration = tool invisible to the LLM. - 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.
- Host-only. Agent tools live under
core/agent/and are never shell-synced. Do not add them tosyncShellRuntimeSources.
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 DkcSettingCards 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:
- 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)) { … } } } }. - Alignment grid.
DkcToggleRow/DkcChoiceRoware full-bleed rows carrying their own 16dp padding. Anything that is not a row — text fields, radios, group labels, buttons, notes — must sit inside aColumn(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 areText(style = labelMedium, color = primary), notDkcSection. - Radio groups are exactly:
Text(labelMedium, primary)label, thenRow(fillMaxWidth().clip(MaterialTheme.shapes.small).clickable { … }.padding(vertical = 4.dp)) { RadioButton; Spacer(4.dp); Text(bodySmall) }. Never wrap a radio group inDkcSection, never attach per-option prose. - 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. 显示状态栏).
- Expansion state ≠ feature state. Never bind a section's
isExpanded/AnimatedVisibility(visible=…)to the feature'senabledflag — expanding a panel must never switch the feature on. Section collapse state is its ownremember { mutableStateOf }. - Conditional sub-blocks (mode swaps, dependent fields) use
AnimatedVisibilitywithCardExpandTransition/CardCollapseTransition, never bareifinside the card body. - Compile ≠ verified. After any card UI change, build + install on the emulator and check the rendered card:
uiautomator dumpelement 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. - 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 nosoftInputModevalue helps there.WindowHelper.applyImmersiveFullscreentherefore keeps the decor fitting system windows on the RESIZE keyboard path below API 30 (systemSOFT_INPUT_ADJUST_RESIZEworks) 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 undershell/srcis 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. RuncheckConfigFieldDrift. - 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 inbuildWebViewBlockandShellWebViewConfig); 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 causesloadNode/loadJniBridgefailure at runtime. - 16KB page alignment.
libnode.soand other large ELF natives must be 16KB-aligned (ElfAligner16k) for Android 15+ devices;node_bridge.cpp/node_launcher.cenable 16KB app-compat beforedlopen. - Node JNI output bridge.
NodeJniOutputBridgeis a stable class referenced by native code; keep its-keepproguard rule so R8 does not renameonOutput. - Crashing FGS when notification channel creation fails. Always use
SafeNotificationChannelsfor 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.
RuntimePermissionSyncderives 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 hardcodeassets/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 letShellUiParityTestverify the flags; config flowing end-to-end is necessary but not sufficient. - Port conflict policy. Local server runtimes must allocate through
PortManagerand 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. CheckToolRegistryFactory.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
./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:shellrelease, full runtime synced fromapp/ - Incremental
ApkBuildCache(FULL/CONTENT_OVERLAY/REUSE_UNSIGNED); encrypted builds always full - Notification channels: polyfill, polling, WebSocket, FCM (BYO Firebase) via existing abstractions
SafeNotificationChannelsfail-soft path for FGSPortManagerconflict policies + real stop handlers across Node/PHP/Python/Go/WordPressLocalDnsBridgeProxywiring 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-alignedlibnode.so+libc++_shared.so; 16KB app-compat before dlopen; stableNodeJniOutputBridgeJNI callback - Go export:
libgo_exec_loader.soembedded; in-app build ENOSPC handling + GOTMPDIR relocation - Go host-side builds on W^X hosts:
GoDirectBuilderreplayscompile/asm/linkas single-shot processes throughHostProcessLauncher(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 throughutil/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 andTranslateBridgecallbacks 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
localExecAllowedso the LLM knows targetSdk>=29 hosts cannot exec app-storage binaries