chore: import upstream snapshot with attribution
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:43:05 +08:00
commit 426e9eeabd
41828 changed files with 9656266 additions and 0 deletions
@@ -0,0 +1,64 @@
ext {
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1'
androidxWebkitVersion = project.hasProperty('androidxWebkitVersion') ? rootProject.ext.androidxWebkitVersion : '1.11.0'
coroutinesVersion = project.hasProperty('coroutinesVersion') ? rootProject.ext.coroutinesVersion : '1.10.2'
}
apply plugin: 'com.android.library'
// Explicitly apply the Kotlin Android plugin. Without it, AGP 8.13 falls back to
// its "built-in Kotlin" compile path, which does NOT bundle the compiled
// classes into the *release* library jar — the Capacitor plugin would then be
// absent from the release dex. Applying the standard plugin wires Kotlin into
// both debug and release jar-bundling.
apply plugin: 'org.jetbrains.kotlin.android'
android {
namespace = "ai.eliza.plugins.browsersurface"
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
defaultConfig {
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = "21"
}
}
repositories {
google()
maven {
url = uri(rootProject.ext.has('mavenCentralMirrorUrl') ? rootProject.ext.mavenCentralMirrorUrl : 'https://repo.maven.apache.org/maven2')
}
mavenCentral()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':capacitor-android')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
// Out-of-process renderer control + multi-profile storage partitioning.
implementation "androidx.webkit:webkit:$androidxWebkitVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
androidTestImplementation "androidx.webkit:webkit:$androidxWebkitVersion"
}
@@ -0,0 +1,79 @@
package ai.eliza.plugins.browsersurface
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.webkit.Profile
import androidx.webkit.ProfileStore
import androidx.webkit.WebViewFeature
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
/**
* Proves — on a real device/emulator — the storage-partitioning primitive the
* plugin's `isolated` policy relies on: a cookie written into one androidx.webkit
* [Profile] is invisible to a sibling profile and to the default (host) profile.
* This is the cross-surface leak the isolation epic (#13452/#15245) closes,
* exercised against the actual system WebView multi-profile store rather than a
* mock. Gated by the MULTI_PROFILE feature (older system WebViews skip); on those
* devices the plugin fails `createSurface` fast rather than degrading silently.
*/
@RunWith(AndroidJUnit4::class)
class BrowserSurfaceIsolationInstrumentedTest {
private val urlA = "https://eliza-surface-a.example/"
private val urlShared = "https://eliza-surface-shared.example/"
@Test
fun cookiesWrittenInAnIsolatedProfileAreInvisibleToSiblingsAndTheDefault() {
assumeTrue(
"multi-profile unsupported on this system WebView",
WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE),
)
val store = ProfileStore.getInstance()
val profileA = store.getOrCreateProfile("eliza-surface-test-a")
val profileB = store.getOrCreateProfile("eliza-surface-test-b")
val cmA = profileA.cookieManager
val cmB = profileB.cookieManager
val cmDefault = store.getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME).cookieManager
cmA.setCookie(urlA, "session=secret-A")
cmA.flush()
// Profile A sees its own cookie…
assertTrue(cmA.getCookie(urlA)?.contains("secret-A") == true)
// …but a sibling profile and the default profile do NOT.
assertNull(cmB.getCookie(urlA))
assertNull(cmDefault.getCookie(urlA))
}
@Test
fun distinctIsolatedProfilesAreDistinctInstances() {
assumeTrue(
"multi-profile unsupported on this system WebView",
WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE),
)
val store = ProfileStore.getInstance()
val a = store.getOrCreateProfile("eliza-surface-test-a")
val b = store.getOrCreateProfile("eliza-surface-test-b")
assertNotEquals(a.name, b.name)
}
@Test
fun sharedStorageUsesTheDefaultProfile() {
assumeTrue(
"multi-profile unsupported on this system WebView",
WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE),
)
val store = ProfileStore.getInstance()
val cmDefault = store.getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME).cookieManager
cmDefault.setCookie(urlShared, "shared=value")
cmDefault.flush()
// A second read of the default profile sees the shared write.
val again = store.getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME).cookieManager
assertEquals(true, again.getCookie(urlShared)?.contains("shared=value"))
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -0,0 +1,245 @@
package ai.eliza.plugins.browsersurface
import android.os.Build
import android.view.View
import android.webkit.WebView
import android.widget.FrameLayout
import androidx.webkit.ProfileStore
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
/**
* Native Android half of `ElizaSurfaceManager` (#15245): layers one [WebView]
* per Browser tab above the Capacitor host webview, each with the platform
* out-of-process renderer and its OWN storage partition, so third-party content
* can never reach the host realm or a sibling tab.
*
* Isolation maps onto two androidx.webkit primitives. Renderer: the WebView
* renderer runs out-of-process by platform default on API 26+; an `isolated`
* surface asserts that separation is actually in effect and fails fast if not.
* Storage: an `isolated` surface gets its own multi-profile [androidx.webkit
* Profile][ProfileStore] (cookies/localStorage/IndexedDB partitioned); a
* `shared` surface uses the default profile. There is NO silent degrade — if the
* system WebView is too old for multi-profile, `createSurface` rejects, because a
* surface that quietly shares the default store is the exact leak this closes.
*/
@CapacitorPlugin(name = "ElizaSurfaceManager")
class ElizaSurfaceManagerPlugin : Plugin() {
private data class Surface(
val webView: WebView,
val process: String,
val storage: String,
var foregrounded: Boolean,
)
private val surfaces = HashMap<String, Surface>()
private fun density(): Float = activity.resources.displayMetrics.density
@PluginMethod
fun createSurface(call: PluginCall) {
val id = call.getString("id") ?: run {
call.reject("createSurface requires an id")
return
}
val process = call.getString("process")
if (process != "isolated" && process != "shared") {
call.reject("createSurface requires an explicit process policy (isolated|shared)")
return
}
val storage = call.getString("storage")
if (storage != "isolated" && storage != "shared") {
call.reject("createSurface requires an explicit storage policy (isolated|shared)")
return
}
val url = call.getString("url")
activity.runOnUiThread {
if (surfaces.containsKey(id)) {
call.resolve()
return@runOnUiThread
}
val host = bridge.webView.parent as? FrameLayout ?: run {
call.reject("host webview has no FrameLayout parent to attach the surface to")
return@runOnUiThread
}
val webView = WebView(activity)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.databaseEnabled = true
// Storage isolation via multi-profile. Fail-fast: no silent degrade
// to the shared default profile on an unsupported system WebView.
if (storage == "isolated") {
if (!WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE)) {
webView.destroy()
call.reject("isolated storage requires WebView multi-profile support; system WebView is too old")
return@runOnUiThread
}
val profile = ProfileStore.getInstance().getOrCreateProfile("eliza-surface-$id")
WebViewCompat.setProfile(webView, profile.name)
}
// shared storage ⇒ the default profile (host-scoped store).
val lp = FrameLayout.LayoutParams(0, 0)
host.addView(webView, lp)
webView.visibility = View.GONE
if (url != null) webView.loadUrl(url)
// Renderer isolation: assert the out-of-process renderer is in effect
// for an isolated surface. On API 26+ the platform runs it in the
// sandboxed :webview_service process; a null handle when the feature
// is supported means this device/build cannot isolate — reject.
if (
process == "isolated" &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
WebViewFeature.isFeatureSupported(WebViewFeature.GET_WEB_VIEW_RENDERER) &&
WebViewCompat.getWebViewRenderProcess(webView) == null
) {
host.removeView(webView)
webView.destroy()
call.reject("isolated process requires an out-of-process WebView renderer, which is unavailable on this device")
return@runOnUiThread
}
surfaces[id] = Surface(webView, process, storage, false)
call.resolve()
}
}
@PluginMethod
fun setBounds(call: PluginCall) {
val id = call.getString("id") ?: run {
call.reject("setBounds requires an id")
return
}
val x = call.getDouble("x") ?: 0.0
val y = call.getDouble("y") ?: 0.0
val width = call.getDouble("width") ?: 0.0
val height = call.getDouble("height") ?: 0.0
activity.runOnUiThread {
val surface = surfaces[id] ?: run {
call.reject("no surface $id")
return@runOnUiThread
}
val d = density()
val lp = FrameLayout.LayoutParams((width * d).toInt(), (height * d).toInt())
lp.leftMargin = (x * d).toInt()
lp.topMargin = (y * d).toInt()
surface.webView.layoutParams = lp
call.resolve()
}
}
@PluginMethod
fun navigate(call: PluginCall) {
val id = call.getString("id")
val url = call.getString("url")
if (id == null || url == null) {
call.reject("navigate requires an id and a url")
return
}
activity.runOnUiThread {
val surface = surfaces[id] ?: run {
call.reject("no surface $id")
return@runOnUiThread
}
surface.webView.loadUrl(url)
call.resolve()
}
}
@PluginMethod
fun foregroundSurface(call: PluginCall) {
val id = call.getString("id") ?: run {
call.reject("foregroundSurface requires an id")
return
}
activity.runOnUiThread {
val surface = surfaces[id] ?: run {
call.reject("no surface $id")
return@runOnUiThread
}
surface.webView.bringToFront()
surface.webView.visibility = View.VISIBLE
surface.foregrounded = true
call.resolve()
}
}
@PluginMethod
fun backgroundSurface(call: PluginCall) {
val id = call.getString("id") ?: run {
call.reject("backgroundSurface requires an id")
return
}
activity.runOnUiThread {
val surface = surfaces[id] ?: run {
call.reject("no surface $id")
return@runOnUiThread
}
surface.webView.visibility = View.GONE
surface.foregrounded = false
call.resolve()
}
}
@PluginMethod
fun destroySurface(call: PluginCall) {
val id = call.getString("id") ?: run {
call.reject("destroySurface requires an id")
return
}
activity.runOnUiThread {
surfaces.remove(id)?.let { surface ->
surface.webView.stopLoading()
(surface.webView.parent as? FrameLayout)?.removeView(surface.webView)
surface.webView.destroy()
}
call.resolve()
}
}
@PluginMethod
fun foregroundHost(call: PluginCall) {
activity.runOnUiThread {
for (surface in surfaces.values) {
surface.webView.visibility = View.GONE
surface.foregrounded = false
}
call.resolve()
}
}
@PluginMethod
fun getSurfaceState(call: PluginCall) {
val id = call.getString("id") ?: run {
call.reject("getSurfaceState requires an id")
return
}
activity.runOnUiThread {
val result = JSObject()
val surface = surfaces[id]
if (surface == null) {
result.put("exists", false)
result.put("foregrounded", false)
result.put("currentUrl", JSObject.NULL)
result.put("process", JSObject.NULL)
result.put("storage", JSObject.NULL)
} else {
result.put("exists", true)
result.put("foregrounded", surface.foregrounded)
result.put("currentUrl", surface.webView.url ?: JSObject.NULL)
result.put("process", surface.process)
result.put("storage", surface.storage)
}
call.resolve(result)
}
}
}