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

30 KiB
Raw Permalink Blame History

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 :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. 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:

  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:

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 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:

  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

./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