diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4ffa07d1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ + +android/gradlew.bat text eol=crlf diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..d6a5c63d --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,189 @@ +name: Android APK and AAB +on: + pull_request: + paths: ['android/**', 'native/**', 'app/**', 'engine/**', 'room/**', 'room.js', 'p2p.html', 'index.html', 'site/**', 'scripts/android-*', 'tests/duga/**', '.github/workflows/android.yml'] + push: + branches: [main, feat/duga-android] + tags: ['android-v*'] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: android-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + oracle: + runs-on: ubuntu-24.04 + timeout-minutes: 25 + env: + DUGA_WEBGPU_MODULE: native/vulkan/oracle/node_modules/webgpu/index.js + DUGA_SOURCE_COMMIT: ${{ github.sha }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: { node-version: 22 } + - name: Install native build and software Vulkan driver + run: sudo apt-get update && sudo apt-get install -y g++ libvulkan-dev mesa-vulkan-drivers spirv-tools + - name: Select the installed software Vulkan ICD + run: | + mapfile -t icds < <(find /usr/share/vulkan/icd.d -maxdepth 1 -type f -name '*lvp*.json' -print) + test "${#icds[@]}" -eq 1 || { echo 'Expected one installed Lavapipe driver'; exit 1; } + printf 'VK_ICD_FILENAMES=%s\n' "${icds[0]}" >> "$GITHUB_ENV" + - name: Install pinned shader compiler and WebGPU oracle + run: | + rustup update stable --no-self-update + cargo +stable install naga-cli --version 30.0.1 --locked + npm ci --prefix native/vulkan/oracle --no-audit --no-fund + - name: Compile original shader sources and Vulkan runners + run: | + node native/vulkan/shaders.mjs + node native/vulkan/export-reference.mjs + g++ -std=c++17 -O2 -ffp-contract=off -Inative/vulkan/build/shaders native/vulkan/vulkan.cpp native/vulkan/oracle.cpp -lvulkan -o native/vulkan/build/duga-vk-oracle + g++ -std=c++17 -O2 -ffp-contract=off -Inative/vulkan/build/shaders native/vulkan/vulkan.cpp native/vulkan/gguf.cpp native/vulkan/dense.cpp native/vulkan/model-run.cpp -lvulkan -o native/vulkan/build/duga-vk-model + - name: Capture WebGPU oracle and require exact Vulkan parity + env: + DUGA_SOURCE_COMMIT: ${{ github.sha }} + run: | + node native/vulkan/capture-oracle.mjs + native/vulkan/build/duga-vk-oracle native/vulkan/build/oracle.dvk + native/vulkan/build/duga-vk-oracle native/vulkan/build/oracle.dvk --reference + native/vulkan/build/duga-vk-oracle native/vulkan/build/oracle.dvk --on-device + node native/vulkan/check-model.mjs + node --test tests/duga/*.test.mjs + - name: Install the Android emulator's software Vulkan driver + uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 + with: + packages: emulator + log-accepted-android-sdk-licenses: false + - name: Capture the Android software-device reference + env: + DUGA_SOURCE_COMMIT: ${{ github.sha }} + run: | + export VK_ICD_FILENAMES="$ANDROID_HOME/emulator/lib64/vulkan/vk_swiftshader_icd.json" + test -f "$VK_ICD_FILENAMES" + node native/vulkan/capture-oracle.mjs + native/vulkan/build/duga-vk-oracle native/vulkan/build/oracle.dvk + native/vulkan/build/duga-vk-oracle native/vulkan/build/oracle.dvk --reference + native/vulkan/build/duga-vk-oracle native/vulkan/build/oracle.dvk --on-device + node native/vulkan/check-model.mjs + - name: Upload validated native shader inputs + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: native-vulkan-inputs + path: | + native/vulkan/build/shaders/ + native/vulkan/build/oracle.dvk + native/vulkan/build/oracle.dvk.json + if-no-files-found: error + retention-days: 14 + + build: + needs: oracle + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: { node-version: 22 } + - uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 + with: { distribution: temurin, java-version: 17 } + - uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 + with: + packages: platform-tools + log-accepted-android-sdk-licenses: false + - uses: gradle/actions/setup-gradle@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: { name: native-vulkan-inputs, path: native/vulkan/build } + - name: Install pinned Android SDK components + run: sdkmanager 'platforms;android-36' 'build-tools;35.0.0' 'ndk;27.3.13750724' 'cmake;3.22.1' + - name: Validate version for tag delivery + if: startsWith(github.ref, 'refs/tags/android-v') + env: { RELEASE_TAG: '${{ github.ref_name }}' } + run: node scripts/android-version.mjs + - name: Configure release signing + if: startsWith(github.ref, 'refs/tags/android-v') + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + test -n "$KEYSTORE_BASE64" && test -n "$KEYSTORE_PASSWORD" && test -n "$KEY_ALIAS" && test -n "$KEY_PASSWORD" || { echo 'Android release signing secrets are required; refusing unsigned tag delivery.'; exit 1; } + printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/duga-release.jks" + chmod 600 "$RUNNER_TEMP/duga-release.jks" + printf 'DUGA_KEYSTORE_PATH=%s\n' "$RUNNER_TEMP/duga-release.jks" >> "$GITHUB_ENV" + - name: Test and lint Android port + run: android/gradlew -p android testDebugUnitTest lintDebug --no-daemon + - name: Enable KVM access for the Android test device + run: | + test -e /dev/kvm + sudo chmod a+rw /dev/kvm + - name: Verify Android Vulkan logits, JNI and bundled UI + uses: ReactiveCircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d + with: + api-level: 35 + target: google_apis + arch: x86_64 + ram-size: 2048M + emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none + script: | + npm ci --prefix native/vulkan/oracle --no-audit --no-fund + VK_ICD_FILENAMES="$ANDROID_HOME/emulator/lib64/vulkan/vk_swiftshader_icd.json" DUGA_WEBGPU_MODULE=native/vulkan/oracle/node_modules/webgpu/index.js node scripts/android-native-check.mjs + android/gradlew -p android connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.requireExactVulkan=true --no-daemon + - name: Build preview APK and AAB + if: ${{ !startsWith(github.ref, 'refs/tags/android-v') }} + run: android/gradlew -p android assembleDebug bundleDebug --no-daemon + - name: Build signed release APK and AAB + env: + DUGA_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + DUGA_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + DUGA_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + if: startsWith(github.ref, 'refs/tags/android-v') + run: | + android/gradlew -p android assembleRelease bundleRelease --no-daemon + "$ANDROID_HOME/build-tools/35.0.0/apksigner" verify android/app/build/outputs/apk/release/app-release.apk + jarsigner -verify android/app/build/outputs/bundle/release/app-release.aab > "$RUNNER_TEMP/aab-signature.txt" + grep -q "jar verified" "$RUNNER_TEMP/aab-signature.txt" + - name: Verify 16 KB native-library ZIP alignment + run: | + mapfile -t apks < <(find android/app/build/outputs/apk -name '*.apk' -type f -print) + test "${#apks[@]}" -gt 0 + for apk in "${apks[@]}"; do + "$ANDROID_HOME/build-tools/35.0.0/zipalign" -c -P 16 4 "$apk" + done + - name: Package delivery files and checksums + run: | + mkdir -p android-delivery + if [[ "$GITHUB_REF" == refs/tags/android-v* ]]; then + cp android/app/build/outputs/apk/release/app-release.apk "android-delivery/duga-android-$DUGA_VERSION_NAME.apk" + cp android/app/build/outputs/bundle/release/app-release.aab "android-delivery/duga-android-$DUGA_VERSION_NAME.aab" + else + cp android/app/build/outputs/apk/debug/app-debug.apk android-delivery/duga-android-debug.apk + cp android/app/build/outputs/bundle/debug/app-debug.aab android-delivery/duga-android-debug.aab + fi + cd android-delivery + sha256sum *.apk *.aab > SHA256SUMS + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: { name: duga-android, path: android-delivery/, if-no-files-found: error, retention-days: 30 } + - name: Remove temporary signing key + if: always() + run: rm -f "$RUNNER_TEMP/duga-release.jks" + + release: + if: startsWith(github.ref, 'refs/tags/android-v') + needs: build + runs-on: ubuntu-24.04 + permissions: { contents: write } + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: { name: duga-android, path: android-delivery } + - name: Publish APK and AAB on the existing tag + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + flags=() + [[ "$RELEASE_TAG" == *-*.*.*-* ]] && flags+=(--prerelease) + gh release create "$RELEASE_TAG" android-delivery/* --verify-tag --title "Duga Android ${RELEASE_TAG#android-v}" --generate-notes "${flags[@]}" diff --git a/.gitignore b/.gitignore index 5bd65369..26b59577 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,16 @@ tests/eval/results/ dist/ docs-site/dist/ docs-site/.astro/ + +# Android/native generated files and signing material +android/.gradle/ +android/.cxx/ +android/build/ +android/app/build/ +android/app/.cxx/ +android/local.properties +native/vulkan/build/ +*.jks +*.keystore +*.apk +*.aab diff --git a/.vercelignore b/.vercelignore index f9f4855f..cb442a7f 100644 --- a/.vercelignore +++ b/.vercelignore @@ -28,3 +28,7 @@ docs-site/.astro /dist + +# Native Android builds are separate from the website. +android +native diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..f25f87ca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,112 @@ +# Duga: Product Direction and Agent Notes + +## Purpose and status + +Duga is a proposed website-first personal AI assistant at **duga.app**. A visitor should be able to open the site and start using an assistant without installing software or setting up a room. Over time, they can connect their own devices, contribute idle compute, and use agents for coding, everyday tasks, memory, voice, and data integrations. + +**The direction below remains a product plan; do not infer shipped functionality from it.** The Duga preview now adds a root-page Local assistant, explicit browser-only notes, session activity and an embedded room workspace. Private Net ownership enforcement, public compute and account services remain unimplemented. The underlying runtime is Pooled: a peer-to-peer inference engine, room runtime, coding-agent harness, CLI/API bridge, and OpenClaw integration. Reuse those components where appropriate; do not represent accounts, the Global Net, credits, long-term personal memory, general connectors, or end-to-end voice conversation as implemented today. + +The existing Pooled project expressly commits to *no accounts* and does not plan to open rooms to strangers by default. Duga's initial product also has **no accounts**: use invite links and room codes, preserving the current browser-room experience. Accounts are deferred, not a prerequisite for continuity or device participation. A public compute network still requires a distinct trust boundary and an explicit governance decision; it must not silently change the security model. See [GOVERNANCE.md](GOVERNANCE.md), [roadmap/README.md](roadmap/README.md), and [SECURITY.md](SECURITY.md). + +## Target experience + +1. Visit **duga.app** and get a useful answer immediately. Guest access should not require an account, installing a worker, pairing a device, or donating compute. +2. Use invite links or room codes to connect another device without registration or login. Preserve the existing distinction: invite links grant access to their holders; code-based requests require host approval by default. Cross-device continuity, durable memory and persistent device management remain proposed and must not imply an account service exists. +3. Let users connect their computers and other capable devices through invite links or host-approved room codes. Show availability, capacity, privacy mode, current jobs, and a one-click disconnect/revoke action. +4. Let users ask for coding help, everyday planning and actions, and voice interaction from one assistant interface. Show what an agent plans to do, what it did, and what requires approval. +5. Provide clear, per-task compute choices: **Local**, **Private Net**, or **Global Net**. Do not make network placement an invisible privacy decision. +6. Let users inspect, correct, export, and delete their memories and connected-service permissions. + +An immediate first answer needs a dependable inference route. An active Global Net alone cannot guarantee availability. Plan a clearly disclosed bootstrap route (such as reserved/hosted capacity or a suitable on-device model); measure cost, privacy, and quality before selecting a default. Never silently send a private task to Global Net because a user's devices are offline. + +## Two networks, plus local execution + +| Mode | Compute membership | Intended use | Data rule | +| --- | --- | --- | --- | +| **Local** | The device currently in use | Tasks that fit locally; offline-capable work where supported | No other compute node receives task data. | +| **Private Net** | The user's own devices explicitly admitted through invite links or host-approved room codes | Personal conversations, memories, coding work, and connected-service data | Only the owner's approved devices may act as inference workers. | +| **Global Net** | Opt-in idle devices contributed by participants; shared pool for participants | Tasks whose inputs the user explicitly agrees to expose to untrusted workers | Treat assigned workers as untrusted. No default access to personal memories, connector credentials, or private files. | + +Private Net membership is **the user's own devices only** in the initial product; invite-based teams are out of scope for now. A link or code does not prove ownership: the user must check which devices they admit. An existing Pooled room can include other people and must not automatically be labeled an owned-device Private Net. The current runtime does not enforce device ownership. Global Net is a participant-shared pool, not a free source of secretly enrolled devices. Contributing compute is optional and separate from using the website. The user can choose a mode per task or conversation; the system must visibly explain when a choice changes where data is processed. + +A Global Net worker may see task text, intermediate activations, outputs, model weights, and operational metadata, depending on the job. **Activations are not encryption**: they may reveal prompt content. Do not claim that public nodes cannot learn users' prompts, or that transport encryption makes a public worker trustworthy. If a task includes potentially sensitive content, recommend Local or Private Net. Require explicit, informed consent before sending it to Global Net. Provenance-based blocking of memory/connector data is defense in depth, not a promise that arbitrary pasted text is automatically classified correctly. See [SECURITY.md](SECURITY.md). + +## Execution architecture + +```text +Browser at duga.app + - account-free sessions, invite links/codes, chat, voice, agent activity, approvals + - per-task compute-mode choice, memories and device controls + | +Control plane (new product services, not the Pooled room broker) + - room-scoped admission and proposed device revocation (no account identity) + - presence, capability discovery, job scheduling and quotas + - contribution/usage ledger and abuse controls + - optional encrypted sync and connector authorization + | +Trusted agent orchestrator (user device or explicitly disclosed service) + - plans and routes tasks, enforces tool permissions and budgets + - owns memory retrieval and service credentials + - submits only permitted inference payloads to the selected compute mode + | +Compute/data plane + - Local: one device + - Private Net: owned and paired devices + - Global Net: opt-in, untrusted volunteer workers +``` + +Keep the control plane and inference data plane separate. An admission or scheduling service should not receive conversation plaintext merely because it handles membership or scheduling; whether it does receive plaintext through a specific fallback or hosted orchestrator must be stated truthfully. Never give Global Net workers connector tokens or unrestricted tool execution. Prefer running agent orchestration and sensitive tools in a trusted environment and using remote nodes only for explicitly approved inference jobs. + +The existing Pooled runtime splits **one model's layers** across GPUs over WebRTC; this is different from running several independent agents. Support both patterns deliberately: model-level distribution for a large model, and agent-level delegation only when tasks can be partitioned, measured, and safely combined. Cross-device latency, the slowest worker, and worker churn can make distribution slower than a single device. Implement scheduling and fallbacks based on measured performance rather than assuming more nodes are faster. See [docs/architecture.md](docs/architecture.md) and [README.md](README.md). + +## Agent capabilities and permissions + +- **Coding:** reuse the existing coding harness and isolated preview where it fits. Present diffs and require approval before touching real projects or running consequential code. Keep generated code sandboxed. +- **Everyday tasks:** start with read-only lookup and drafts, then add calendar/email/file changes behind review and explicit confirmation. Maintain an audit trail, cancellation, retry limits, and clear failure states. +- **Memory:** separate transient conversation context from durable, user-visible facts. Request or clearly present proposed saves; support correction, deletion, and scoped retrieval. Never assume a fact inferred by the model is true. +- **Voice:** add microphone permission, speech-to-text, spoken responses, interruption, and visible recording state. Begin with push-to-talk rather than always-on listening. Browser speech synthesis exists in the room, but voice conversation and audio API support are not already complete. +- **Data integrations:** add connectors one at a time with narrowly scoped OAuth or equivalent grants. Store credentials in a trusted boundary, never in prompts sent to volunteer nodes. Show what each connector may read or change, and allow revocation. +- **Multiple agents:** begin with one orchestrator and bounded tool loops. Add specialist/parallel agents only where evaluation shows a benefit, and keep a single authority for permissions, budgets, and final actions. + +Tool results and model output are untrusted. Validate arguments and schemas, isolate execution, bound steps/time/cost, and distinguish a proposed action from an action already performed. Do not automatically execute a volunteer worker's code or recommendations on a user's machine. + +## Contributed-node experience and credits + +A device owner must explicitly opt in to Global Net contribution and retain control over resource limits, allowed hours, idle/battery/thermal behavior, downloads, and immediate stop. A browser tab may contribute while active and supported; a closed or suspended tab is **not** an always-on worker. A future desktop or background node is an optional upgrade, not a prerequisite for using duga.app. + +Use **non-cash contribution credits** initially: earn credits by providing verified work and spend them on Global Net jobs; show earnings, charges, balances, quotas, and job history. Provide a bounded way for guests and newcomers to try the service without first donating compute. Define exact accounting rates after benchmarking and abuse analysis. Do not mint credits merely for claiming idle capacity or self-reported work; account for verifiable completed work, failed jobs, duplicate identities, manipulated results, and disputes. Correct execution by an arbitrary public worker is not guaranteed by the existing Pooled system; redundancy/spot checks and reputation can reduce risk, not prove universal correctness. Do not promise monetary redemption or a trustless economy. + +Public contribution also entails risks for providers: untrusted inputs, model and dependency downloads, disk use, power, heat, bandwidth, and potentially abusive workloads. Constrain jobs and isolate any code execution; do not turn a volunteer inference worker into a general remote shell. + +## Delivery sequence + +1. **Decision record and evaluation baseline:** define account-free session and invitation boundaries, guest inference bootstrap, explicit Global Net consent, data-retention policy, model licenses, target devices, task examples, cost and latency targets. Resolve Duga-versus-Pooled governance before changing core product commitments. +2. **Website-first assistant:** usable guest chat at duga.app with an honest, dependable inference route; responsive UI, streaming, visible privacy mode, and basic session handling. +3. **Private Net:** invite-link/code admission, key and device revocation, an explicit owned-device trust policy, offline handling, local/private routing, and quality/performance comparison against one device. +4. **Useful agents:** coding flow, limited everyday tools, user-controlled memory, push-to-talk voice, and at least one read-only connector. Add approvals and observable execution before write-capable integrations. +5. **Global Net pilot:** explicitly enrolled nodes and opted-in jobs, limited model/job types, admission and scheduling, rate limits, disconnect recovery, privacy warnings, abuse controls, and measured correctness/latency. Start with a restricted cohort, not an open public network. +6. **Credits and scale:** transparent ledger, verified-work policy, allocation fairness, audits, and multi-agent or background-task expansion once the pilot's reliability and security are demonstrated. + +This ordering is a planning recommendation, not a statement that later capabilities already exist. Parallel research is fine, but do not expose high-risk public workloads before the trust model and safeguards exist. + +## Working on this repository + +- Existing building blocks include `engine/` (WebGPU inference), `room.js` and `room/` (P2P room and protocol), `harness/` (Code mode), `cli/` (local compatible API), and `packages/openclaw/` (agent integration). Read [docs/architecture.md](docs/architecture.md), [docs/design/harness-app.md](docs/design/harness-app.md), and [docs/agents.md](docs/agents.md) before modifying them. +- Preserve the existing browser-link room flow and current output-correctness guarantees. Core protocol, peer visibility, dependencies, and model changes require a design note and maintainer review under [GOVERNANCE.md](GOVERNANCE.md). +- Start with tests that match the layer being changed. The existing repository provides `npm test` (Deno unit tests), package tests, and browser/room end-to-end suites; see [package.json](package.json) and [docs/agents.md](docs/agents.md). Record hardware, commit, workload, and baseline for performance claims. +- Keep **implemented**, **proposed**, and **open decision** distinct in code comments, documentation, and the UI. In particular, a Pooled invite link grants bearer access and does not prove device ownership or a persistent identity, and the existing room authentication does not make public volunteer nodes safe for private prompts. + +## Invitation and session rules + +- **Implemented:** a room code locates a room; host approval is required by default. An invite link contains a random secret in its fragment and admits its holder without approval. Returning devices can use a room pass. See [SECURITY.md](SECURITY.md) and [docs/protocol.md](docs/protocol.md). +- **Required direction:** no registration, login, account database, or account-bound device enrollment in the initial Duga product. Keep invite secrets out of logs, analytics and server-visible URLs. Do not treat short room codes as authentication secrets or silently disable host approval. +- **Proposed:** invitation expiry/rotation, per-device removal and durable cross-device continuity need explicit lifecycle and recovery designs before being advertised. Do not claim current links expire or that disconnecting a device revokes its credentials. +- **Privacy:** every admitted inference device is trusted with room data under the existing threat model. Invitations do not make public workers safe for private tasks. Keep Global Net enrollment and task consent separate from room admission. + +## Open decisions requiring explicit agreement + +- What guarantees the guest's first response: on-device inference, reserved hosted capacity, or another clearly disclosed fallback? Who pays for it? +- Which data is synced across devices, where is it stored, how is it encrypted and deleted, and how can users transfer sessions through explicitly approved devices without accounts? +- What work is admissible on Global Net, what exactly does a worker learn, and what verification is sufficient before credits are awarded? +- How do account-free invitations, device ownership, revocation, recovery, rate limits, and Sybil resistance work without degrading the existing account-free Pooled rooms? +- Which first connector and voice stack provide useful UX without leaking credentials or promising unsupported browser background behavior? diff --git a/README.md b/README.md index 7f3c4f17..494368e9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@
-Peer-to-peer LLM inference in the browser. Pool your devices to run big open models, for chat and coding agents.
+Your devices. Your assistant. Account-free chat, local inference and trusted-device coding rooms.
- Site · - Start a room · + Site · + Open a room · Architecture · Benchmarks · Roadmap · @@ -23,7 +23,17 @@ https://github.com/user-attachments/assets/5142d877-e4d5-4f6b-9b5e-0ef48dcb73f6
Demo, recorded September 27, 2026 (2:29, with voice and subtitles): Qwen 3.6 35B MoE across a Mac and an iPhone in browser tabs. The phone chats with it, then Code mode builds a Tetris game, fixes its own bug and restyles it. Download · the September 7 demo.
-Pooled runs one open model across the devices in a room. Your laptop, a friend's desktop and a phone each hold some of the model's layers, and together they run a model none of them could run alone. You can chat with it, or switch to Code mode and let it build and fix a small app right in the tab. There is nothing to install and no account, and no server does any of the thinking. +The underlying Pooled runtime runs one open model across the devices in a room. Your laptop, a friend's desktop and a phone each hold some of the model's layers, and together they run a model none of them could run alone. You can chat with it, or switch to Code mode and let it build and fix a small app right in the tab. There is nothing to install and no account, and no server does any of the thinking. + +## Duga preview + +Duga is an account-free browser assistant built on the Pooled runtime. The root page provides local streaming chat, explicit browser-only notes, session activity and a room workspace. It downloads Qwen3 0.6B weights on first use and runs on WebGPU; there is no hosted inference fallback. Invite links and host-approved codes connect devices through the existing `/room` and `/r/:code` routes. + +Private Net ownership enforcement, Global Net, credits, service connectors and microphone conversation remain proposed. See [the Duga design note](docs/design/duga.md) for implementation boundaries, privacy and validation. The upstream Pooled CLI/package names and wire/storage identifiers are preserved for compatibility. + +## Android Vulkan preview + +The [Android port](android/README.md) bundles the Duga UI and a native Vulkan Dense backend; Local does not require WebView WebGPU. APK/AAB builds and tag-based GitHub Releases are configured in [Android Actions](.github/workflows/android.yml). Native inference is experimental, uses a strict oracle gate, and currently supports Qwen3 0.6B Q8 only. Production signing requires repository Secrets. ## How it works diff --git a/android/README.md b/android/README.md new file mode 100644 index 00000000..f1e73b88 --- /dev/null +++ b/android/README.md @@ -0,0 +1,91 @@ +# Duga Android preview + +An installable Android port with the Duga UI bundled in the APK and **direct native Vulkan inference** through JNI. The UI is a WebView; Local computation does not use WebView WebGPU. Android 10/API 29 or later, arm64-v8a and x86_64. This initial backend supports Qwen3 Dense Q8_0/F32 GGUF only; no MoE, DeltaNet, native distributed peers or speculative decoding. + +The first Local task asks to enable the experimental backend and runs an exact kernel-oracle gate: the production Naga shaders and independently compiled original Dawn/Tint shaders execute the same synthetic inputs on this device’s GPU. Dawn and WebGPU are build/test tools, never APK runtime dependencies. A mismatch or unavailable driver leaves Local unavailable; there is no tolerance-based acceptance or hosted fallback. First successful setup downloads about 640 MB of pinned, SHA-256-verified Qwen3 0.6B weights to app-private storage. Context is 1,024 tokens and responses are capped at 384 tokens. Device memory, speed and real-phone quality are not guaranteed. Trusted rooms remain a separate compute choice. APK rooms are chat clients of another trusted device; WebGPU contribution is disabled in the APK until native distributed Vulkan workers are implemented. + +## Build locally + +Install JDK 17, Node 22, Rust, Android SDK 36, build-tools 35.0.0, NDK 27.3.13750724 and CMake 3.22.1. Set `JAVA_HOME` and `ANDROID_HOME` normally. From the repository root: + +```sh +cargo install naga-cli --version 30.0.1 --locked +npm ci --prefix native/vulkan/oracle +export DUGA_WEBGPU_MODULE=native/vulkan/oracle/node_modules/webgpu/index.js +node native/vulkan/shaders.mjs +node native/vulkan/export-reference.mjs # requires spirv-as and spirv-val +node native/vulkan/capture-oracle.mjs +android/gradlew -p android testDebugUnitTest lintDebug assembleDebug bundleDebug +``` + +Capturing/calibrating the oracle requires a working WebGPU/Vulkan adapter and SPIR-V Tools (`spirv-as`, `spirv-val`; also available in the pinned NDK shader-tools directory through `SPIRV_AS`/`SPIRV_VAL`). CI calibrates both compiled programs against actual WebGPU output on Lavapipe and SwiftShader. Captured floating-point bytes are device-specific. The APK uses the captured synthetic inputs but recomputes its expected output with the original Tint program on the current GPU; every bit mismatch still fails. `syncWebAssets` automatically copies the allowlisted runtime assets, native Android shim and captured oracle; it never copies model weights or repository metadata. + +Outputs: + +- `app/build/outputs/apk/debug/app-debug.apk` — installable development-signed APK. +- `app/build/outputs/bundle/debug/app-debug.aab` — development bundle, not a production/Play Store upload. + +For a SwiftShader emulator, capture its matching software reference and check the native Android binaries: + +```sh +export VK_ICD_FILENAMES="$ANDROID_HOME/emulator/lib64/vulkan/vk_swiftshader_icd.json" +export DUGA_WEBGPU_MODULE=native/vulkan/oracle/node_modules/webgpu/index.js +node native/vulkan/export-reference.mjs +node native/vulkan/capture-oracle.mjs +DUGA_ANDROID_SERIAL=emulator-5554 node scripts/android-native-check.mjs +android/gradlew -p android connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.requireExactVulkan=true +``` + +The script builds Android command-line runners with the pinned NDK, replays all kernel traces on the device and compares every tiny-model logit with original WebGPU execution. It produces test-only GGUF/greedy fixtures for instrumented JNI tests. Those fixtures are included only in the test APK. Instrumentation covers bundled UI, explicit notes, scoped bridge presence, tied/untied JNI decoding, cancellation/reset and missing-model rejection. CI requires oracle status 0; unknown real devices may fail shader equivalence/capability checks and leave Local unavailable. A passed kernel check is not a universal model-quality or physical-phone compatibility guarantee. Android 15/x86_64 SwiftShader passed all twelve kernels, both tiny models and a separate real Qwen3 0.6B three-position comparison (455,808 exact logits). This does not establish physical-phone performance or universal mobile GPU equivalence. + +## GitHub Actions and tag delivery + +[Android workflow](../.github/workflows/android.yml) runs on pull requests, `main`, `feat/duga-android`, manual dispatch, and `android-v*` tags. It: + +1. Compiles the original WGSL to SPIR-V using pinned Naga 30.0.1. +2. Executes the original WGSL on Dawn WebGPU, then requires bit-exact Vulkan parity for twelve kernels and all logits of tied/untied tiny Dense models, separately on Lavapipe and SwiftShader. Exported Tint programs are calibrated against those same captured bytes before they may be used as the on-device oracle. +3. Runs Node packaging/version tests, Java policy tests and Android lint. Boots an Android 15/x86_64 SwiftShader emulator and requires exact device kernel/logit parity plus instrumented JNI/UI tests before delivery. +4. Builds APK and AAB for arm64/x86_64, checks ZIP alignment for 16 KB pages, and uploads them with SHA256SUMS as the `duga-android` Actions artifact. +5. For a distribution tag, signs release APK/AAB, verifies their signatures, then publishes the files to GitHub Releases. Tags with a prerelease suffix create prereleases. Browser/Vercel deployment is unchanged. + +Configure these repository Secrets before distribution: + +| Secret | Value | +| --- | --- | +| `ANDROID_KEYSTORE_BASE64` | Base64-encoded existing release JKS/PKCS12 keystore | +| `ANDROID_KEYSTORE_PASSWORD` | Keystore password | +| `ANDROID_KEY_ALIAS` | Signing alias | +| `ANDROID_KEY_PASSWORD` | Key password | + +A missing signing secret **fails tag delivery**; no unsigned artifact is published as a release. The key is decoded only into runner temporary storage and deleted afterwards. Retain the real signing key outside CI: future updates require the same identity. Signing keys and recovery credentials are kept outside the repository and are never bundled with the app. + +Once the workflow is on the remote branch and signing Secrets are configured: + +```sh +git tag -a android-v0.1.0-preview.1 -m 'Duga Android Vulkan preview' +git push origin android-v0.1.0-preview.1 +``` + +Tag format is validated before signing. `versionName` comes from the tag, and `versionCode` from the workflow run number (monotonic within this workflow). AAB delivery is to GitHub Releases; this does not automatically publish to Google Play. A version tag is not pushed automatically by the coding agent. + +## Invitations, storage and lifecycle + +- Existing room codes retain host approval by default; invite links retain bearer access. `duga://join/ABC234#k=…` opens the app. HTTPS `duga.app/r/…` links are declared **unverified**; Android may leave them in the browser until domain association and signing identity are deliberately configured. Links can always be pasted into Devices. Native asset origins are rewritten to `https://duga.app` for shared room links and QR codes. +- Native calls are origin-scoped to bundled app pages. Inference calls are main-frame only; opaque sandbox previews have no native bridge. External HTTPS pages open in a browser. File/content/intent navigation, cleartext traffic, automatic camera/microphone permissions and app backups are disabled. +- Notes use the app's private WebView profile, separate from browser storage. No account or sync is added. Uninstalling/clearing app data removes notes and downloaded weights. Exports use Android's document picker; copy/share expose no file-read or arbitrary tool capability. +- Configuration changes such as rotation retain the live WebView, conversation and draft in memory. Invitation fragments and WebView history are not saved into instance-state bundles; process death can still lose the session. +- Backgrounding cancels native compute and disconnects the room workspace. App-owned document pickers and share choosers preserve the workspace until they return, including cancellation. Android Back asks before navigating through page history or closing the app. This is a foreground app, not an always-on worker. Chat is ephemeral; room work may need reopening after an interruption. Disconnecting is not credential revocation. + +See [the design and validation record](../docs/design/duga-android.md) for scope and limitations. + +## Manual real-model UI oracle + +For a dedicated emulator with a debug APK installed, `check-model.mjs` can also generate the full chat oracle. Set `DUGA_REAL_GGUF`, `DUGA_REAL_CONFIG`, `DUGA_CHAT_TOKENIZER` and `DUGA_WEBGPU_MODULE` to verified local files; optionally set `DUGA_NATIVE_RUNNER`, `DUGA_ANDROID_SERIAL` and `ADB` to replay every logit through the Android executable. `node native/vulkan/check-model.mjs` writes `native/vulkan/build/chat-oracle.json` and fails on any logit bit difference. Then run: + +```sh +DUGA_ANDROID_SERIAL=emulator-5554 node tests/e2e/android-native-chat.mjs +``` + +This restarts Duga on the dedicated emulator, approves its native-backend confirmation, disables WebView WebGPU, submits the oracle’s prompt through the real UI and requires the exact original answer. The normal pinned-model preparation/download still runs; no model or inference call is mocked. The screenshot is saved under `native/vulkan/build/`. Use hardware GPU acceleration for the real 0.6B test; software GPU chat can take much longer. This manual check is separate from CI’s bounded tiny-model tests. + +To check continuation after backgrounding, save the first `chat-oracle.json` under another filename, then generate a second oracle with `DUGA_CHAT_PREVIOUS` pointing to the first file and `DUGA_CHAT_PROMPT` set to the follow-up. Set `DUGA_CHAT_ORACLE` and `DUGA_CHAT_FOLLOWUP_ORACLE` to those files when running the UI test. It backgrounds/resumes the app between the two exact-answer checks. Generated assistant token IDs stay with the in-memory conversation across GPU model teardown; starting a new chat or deleting chat clears that history. This is transient session handling, not account-based or durable cross-device continuity. diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..ac6661db --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,76 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'app.duga.android' + compileSdk 36 + + defaultConfig { + applicationId 'app.duga.android' + minSdk 29 + targetSdk 36 + versionCode Integer.parseInt(System.getenv('DUGA_VERSION_CODE') ?: '1') + versionName System.getenv('DUGA_VERSION_NAME') ?: '0.1.0-preview' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + ndk { abiFilters 'arm64-v8a', 'x86_64' } + externalNativeBuild { cmake { cppFlags '-std=c++17 -ffp-contract=off'; arguments '-DANDROID_STL=c++_static', '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON' } } + } + signingConfigs { + release { + def keyPath = System.getenv('DUGA_KEYSTORE_PATH') + if (keyPath) { + storeFile file(keyPath) + storePassword System.getenv('DUGA_KEYSTORE_PASSWORD') + keyAlias System.getenv('DUGA_KEY_ALIAS') + keyPassword System.getenv('DUGA_KEY_PASSWORD') + } + } + } + buildTypes { + release { + minifyEnabled false + if (System.getenv('DUGA_KEYSTORE_PATH')) signingConfig signingConfigs.release + } + } + ndkVersion '27.3.13750724' + externalNativeBuild { cmake { path file('../../native/vulkan/CMakeLists.txt'); version '3.22.1' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + buildFeatures { buildConfig true } + sourceSets.main.assets.srcDirs = [layout.buildDirectory.dir('generated/web-assets')] + // Oracle models belong to the test APK only, never the distributed app. + sourceSets.androidTest.assets.srcDirs = [new File(rootProject.projectDir.parentFile, 'native/vulkan/build/fixtures')] + testOptions.unitTests.returnDefaultValues = true + lint { + abortOnError true + checkReleaseBuilds true + } +} + +def repoRoot = rootProject.projectDir.parentFile + +tasks.register('syncWebAssets', Exec) { + workingDir repoRoot + commandLine 'node', 'scripts/android-assets.mjs', '--out', layout.buildDirectory.dir('generated/web-assets').get().asFile.absolutePath + inputs.file new File(repoRoot, 'native/vulkan/build/oracle.dvk') + inputs.files(fileTree(repoRoot) { + include 'index.html', 'p2p.html', 'room.js', 'favicon.svg', 'site.webmanifest', 'LICENSE' + include 'app/**', 'engine/**', 'room/**', 'harness/**', 'site/**', 'android/web/**' + include 'scripts/android-assets.mjs' + }) + outputs.dir layout.buildDirectory.dir('generated/web-assets') +} +tasks.named('preBuild').configure { dependsOn 'syncWebAssets' } +tasks.configureEach { if (name.startsWith('configureCMake') || name.startsWith('buildCMake')) dependsOn 'syncWebAssets' } + +dependencies { + implementation 'androidx.webkit:webkit:1.14.0' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.json:json:20240303' + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' +} diff --git a/android/app/src/androidTest/java/app/duga/android/PortSmokeTest.java b/android/app/src/androidTest/java/app/duga/android/PortSmokeTest.java new file mode 100644 index 00000000..45c3cc9e --- /dev/null +++ b/android/app/src/androidTest/java/app/duga/android/PortSmokeTest.java @@ -0,0 +1,215 @@ +package app.duga.android; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import android.webkit.WebView; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.os.SystemClock; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.inspector.WindowInspector; +import org.junit.Test; +import org.junit.runner.RunWith; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; + +@RunWith(AndroidJUnit4.class) +public class PortSmokeTest { + private String js(ActivityScenario