Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/release-mobile-lab-control.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: release Mobile Lab Control

on:
push:
tags:
- "mobile-lab-control-v*"

permissions:
contents: write

jobs:
release-apk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
- name: Verify Android build inputs
run: |
test -f "$ANDROID_SDK_ROOT/platforms/android-35/android.jar"
test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/aapt2"
test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/apksigner"
- name: Restore release signing key
env:
KEYSTORE_B64: ${{ secrets.MOBILE_LAB_CONTROL_KEYSTORE_B64 }}
run: |
test -n "$KEYSTORE_B64"
umask 077
printf '%s' "$KEYSTORE_B64" | base64 --decode > "$RUNNER_TEMP/mobile-lab-control-release.jks"
- name: Build release APK
env:
ANDROID_RELEASE_KEYSTORE: ${{ runner.temp }}/mobile-lab-control-release.jks
ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.MOBILE_LAB_CONTROL_KEY_ALIAS }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.MOBILE_LAB_CONTROL_KEYSTORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.MOBILE_LAB_CONTROL_KEY_PASSWORD }}
run: |
version=${GITHUB_REF_NAME#mobile-lab-control-v}
IFS=. read -r major minor patch <<< "$version"
version_code=$((major * 10000 + minor * 100 + patch))
VERSION_NAME="$version" VERSION_CODE="$version_code" BUILD_MODE=release apps/mobile-lab-control/build.sh
cd apps/mobile-lab-control/build
sha256sum "mobile-lab-control-$version.apk" > "mobile-lab-control-$version.apk.sha256"
- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
version=${GITHUB_REF_NAME#mobile-lab-control-v}
apk="apps/mobile-lab-control/build/mobile-lab-control-$version.apk"
checksum="$apk.sha256"
gh release create "$GITHUB_REF_NAME" "$apk" "$checksum" \
--verify-tag \
--title "Mobile Lab Control $version" \
--notes "Installable release-signed APK and SHA-256 checksum for the three independent Happy, llama.cpp, and model-profile widgets. The app has no Internet permission. / 三个独立 Happy、llama.cpp、模型配置微件的正式签名 APK 与 SHA-256;App 不申请联网权限。"
32 changes: 31 additions & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,35 @@ jobs:
repository:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- run: ./scripts/verify.sh

android-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
- name: Verify Android build inputs
run: |
test -f "$ANDROID_SDK_ROOT/platforms/android-35/android.jar"
test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/aapt2"
test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/apksigner"
- name: Build debug-signed verification APK
run: BUILD_MODE=debug apps/mobile-lab-control/build.sh
- name: Verify package permissions and signature
run: |
apk=apps/mobile-lab-control/build/mobile-lab-control-0.4.0-debug.apk
permissions=$("$ANDROID_SDK_ROOT/build-tools/35.0.0/aapt2" dump permissions "$apk")
printf '%s\n' "$permissions"
! grep -q 'android.permission.INTERNET' <<< "$permissions"
grep -q 'com.termux.permission.RUN_COMMAND' <<< "$permissions"
"$ANDROID_SDK_ROOT/build-tools/35.0.0/apksigner" verify --verbose "$apk"
- uses: actions/upload-artifact@v4
with:
name: mobile-lab-control-debug-verification
path: apps/mobile-lab-control/build/mobile-lab-control-0.4.0-debug.apk
if-no-files-found: error
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
.env
secrets/
evidence/private/
apps/mobile-lab-control/build/
*.jks
*.keystore
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ OpenCode TUI/Web ---------------------+-> provider profile -> model API
loopback OpenAI-compatible API for lightweight offline tasks.
- Switching model profiles is explicit, keeps secrets outside committed
configuration, and never changes service lifecycle implicitly.
- Termux:Widget exposes three separate status-bearing controls for Happy,
llama.cpp, and model configuration.
- The offline `Mobile Lab Control` app exposes three independent native widgets
for Happy, llama.cpp, and model profiles. Termux:Widget remains an opt-in
terminal fallback.

## Repository layout

- `skills/deploy-android-ai-stack/` — install and operate the runtime.
- `skills/validate-android-ai-stack/` — bounded, evidence-based acceptance
checks.
- `apps/mobile-lab-control/` — dependency-free Android AppWidget source and
reproducible build script.
- `config/profiles/` — safe provider-profile templates.
- `docs/` — architecture and the verified OnePlus 8T case.
- `manifests/` — facts captured from tested devices, with secret-free values.
Expand All @@ -52,7 +55,9 @@ OpenCode TUI/Web ---------------------+-> provider profile -> model API
[SKILL.md](skills/deploy-android-ai-stack/SKILL.md).
3. Create a provider profile from one of `config/profiles/*.example` and put
the token in the referenced private file.
4. Use `$validate-android-ai-stack` to run zero-call checks first, then opt in
4. Install the native control bridge and a checksum-verified APK from
[GitHub Releases](https://github.com/toolazytoname/android-ai-stack/releases).
5. Use `$validate-android-ai-stack` to run zero-call checks first, then opt in
to at most one model call per client.

For an on-phone checklist, use the bilingual
Expand All @@ -77,6 +82,7 @@ fallback have both been exercised. Exact versions and evidence are recorded in
| Phone-local Happy | exercised end to end | lifecycle wrapper included |
| Provider switching | exercised on the private device config | redacted generic profiles included |
| llama.cpp local model | Qwen3 0.6B Q8, API and `local-lite` passed | authenticated loopback lifecycle and bounded checker included |
| Native control widgets | three independent widgets exercised with zero model calls | source, allow-list bridge, bilingual UI, and release APK included |

The final phone acceptance also exercised the control split: profile changes
left running services untouched, Happy reported `STALE` when its startup
Expand Down
18 changes: 12 additions & 6 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
Claude Code、Happy agent 与 Happy server 都运行在手机上;模型推理可以像
电脑一样,通过 URL/token 使用云 API、私有中转或可选的本地小模型。

本仓只发布可复用的部署、模型切换和验收经验,不包含真实 token、私人
endpoint、CC Switch 数据库、Happy 上游源码、签名 APK 或模型权重。
本仓只发布可复用的部署、模型切换和验收经验,不包含真实 Token、私人
Endpoint、CC Switch 数据库、Happy 上游源码或模型权重。正式签名 APK 只作为
GitHub Release 资产发布,不进入 Git 历史。

## 架构

Expand All @@ -28,13 +29,14 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API
- 本地推理也是可选扩展:小型 llama.cpp server 只监听 loopback,适合离线
摘要、分类、脱敏和断网兜底。
- 模型 profile 只保存路由信息;凭据始终位于独立的 mode `0600` 文件。
- 配置切换不会隐式启停服务;Termux:Widget 分别提供 Happy、llama.cpp、Model
三个带状态入口
- 离线 `Mobile Lab Control` App 分别提供 Happy、llama.cpp、模型配置三个
原生微件;Termux:Widget 只保留为可选终端回退

## 仓库内容

- `skills/deploy-android-ai-stack/`:安装、模型切换和本地服务生命周期。
- `skills/validate-android-ai-stack/`:有调用预算的真实验收。
- `apps/mobile-lab-control/`:无第三方依赖的 Android AppWidget 源码与可复建脚本。
- `config/profiles/`:直连、中转、本地 OpenAI-compatible 模板。
- `docs/`:架构、OnePlus 8T 案例和手机端速查教程。
- `manifests/`:不含身份信息的设备观察记录。
Expand All @@ -46,8 +48,11 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API
2. 按 `$deploy-android-ai-stack` 安装固定版本的上游组件和本仓 wrapper。
3. 复制 `config/opencode.example.json`,从模板创建 mode `0600` 的 profile
和 token 文件。
4. 先执行 `quick`、`tui`、`web-start` 等零模型调用检查。
5. 前置全通过后,再分别允许 OpenCode 与 Claude 各做一次短请求。
4. 安装原生控制桥接,并从
[GitHub Releases](https://github.com/toolazytoname/android-ai-stack/releases)
下载和核对 APK。
5. 先执行 `quick`、`tui`、`web-start` 等零模型调用检查。
6. 前置全通过后,再分别允许 OpenCode 与 Claude 各做一次短请求。

适合直接放在手机上边看边操作的版本见
[手机速查教程](docs/phone-quickstart.zh-CN.md)。
Expand All @@ -61,6 +66,7 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API
| 手机本地 Happy | 已做端到端验证 | 提供显式启停 wrapper |
| Provider 切换 | 私有设备配置已验证 | 提供脱敏通用 profile |
| llama.cpp 本地模型 | Qwen3 0.6B Q8 的 API 与 `local-lite` 已通过 | 提供带认证的 loopback wrapper 与限次 checker |
| 原生控制微件 | 三个独立微件已完成零模型调用真机验收 | 提供源码、白名单桥接、双语 UI 与 Release APK |

最终手机验收也覆盖了控制面解耦:切换 profile 不改变正在运行的服务,Happy
启动时 profile 与当前配置不一致时明确显示 `STALE`,两个服务都能在不改变
Expand Down
10 changes: 10 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,15 @@ Prefer text assertions and redacted command output. Capture screenshots only
when a GUI state cannot be proved otherwise. Run `scripts/verify.sh` before a
commit; it detects common secret-shaped strings and accidental model files.

## APK release identity

Installable Mobile Lab Control APKs belong in GitHub Releases, never in source
history. Release APKs must match the public certificate in
`apps/mobile-lab-control/release-certificate.pem`; its SHA-256 certificate
fingerprint is
`D0:2C:A4:E7:A4:FB:74:7A:E4:46:92:1A:DD:19:B1:1D:83:65:EE:3E:29:30:A4:CA:2B:25:94:AB:CE:74:70:45`.
Do not publish a debug-signed APK as a product release. Keep the private key
outside Git and back it up separately from GitHub Actions secrets.

Report a suspected vulnerability privately through GitHub's security advisory
feature rather than a public issue.
64 changes: 64 additions & 0 deletions apps/mobile-lab-control/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.github.toolazytoname.mobilelabcontrol">
<uses-permission android:name="com.termux.permission.RUN_COMMAND" />

<queries>
<package android:name="com.termux" />
</queries>

<application
android:allowBackup="false"
android:icon="@drawable/ic_mobile_lab"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="false">

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<receiver
android:name=".CommandResultReceiver"
android:exported="false" />

<receiver
android:name=".HappyWidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_happy_info" />
</receiver>

<receiver
android:name=".LlamaWidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_llama_info" />
</receiver>

<receiver
android:name=".ModelWidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_model_info" />
</receiver>
</application>
</manifest>
56 changes: 56 additions & 0 deletions apps/mobile-lab-control/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Mobile Lab Control

[English](README.md) · [简体中文](README.zh-CN.md)

An offline Android control surface for the Android AI Stack. It exposes three
independent home-screen widgets:

- **Happy** — status, start/stop, restart, and refresh;
- **llama.cpp** — status, start/stop, memory release, and refresh;
- **model profiles** — current readable profile plus one-tap configuration
switching without changing either service lifecycle.

The app has no Internet permission, reads no token, holds no root privilege,
and accepts no free-form command. It can invoke only
`$PREFIX/bin/mobile-lab-widget-action` through Termux's documented
[`RUN_COMMAND` interface](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent).
The Termux bridge independently validates the same allow-list.

## Build

Install Android platform 35, build-tools 35.0.0, and a JDK, then run:

```bash
BUILD_MODE=debug ./build.sh
```

Supported modes are `debug`, `unsigned`, and `release`. Release signing reads
the keystore path, alias, and passwords from environment variables; no key or
password belongs in Git. Published APKs are attached to GitHub Releases rather
than committed to repository history.

The public release certificate is
[`release-certificate.pem`](release-certificate.pem). Its SHA-256 certificate
fingerprint is
`D0:2C:A4:E7:A4:FB:74:7A:E4:46:92:1A:DD:19:B1:1D:83:65:EE:3E:29:30:A4:CA:2B:25:94:AB:CE:74:70:45`.
Verify future APKs against this identity as well as the attached file checksum.

## Runtime setup

1. Install the Android AI Stack operator scripts in Termux.
2. Review the external-command boundary, then run
`install-control-bridge --enable-external-apps`.
3. Install the APK and grant `com.termux.permission.RUN_COMMAND` when Android
prompts.
4. Open the app and add Happy, llama.cpp, and model-profile widgets separately.

Copy [`config/widget-profiles.example`](../../config/widget-profiles.example) to
`~/.config/android-ai-stack/widget-profiles`, keep it mode `0600`, and replace
only the redacted profile names and labels. The bridge never reads provider
tokens or CC Switch databases.

## Upgrade identity

An APK signed with a debug key cannot be upgraded in place to the public
release-signed build. Remove the debug build once, install the release build,
and re-add the widgets. Future release-signed versions then upgrade normally.
47 changes: 47 additions & 0 deletions apps/mobile-lab-control/README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Mobile Lab Control

[English](README.md) · [简体中文](README.zh-CN.md)

这是 Android AI Stack 的离线原生控制面,提供三个彼此独立的桌面微件:

- **Happy**:状态、启动/关闭、重启与刷新;
- **llama.cpp**:状态、启动/关闭、释放模型内存与刷新;
- **模型配置**:显示当前可读名称,一键切换配置,但不改变两个服务的启停状态。

App 不申请联网权限、不读取 Token、不持有 root,也没有任意命令输入。它只通过
Termux 官方 [`RUN_COMMAND` 接口](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent)
调用固定脚本
`$PREFIX/bin/mobile-lab-widget-action`,Termux 桥接层会再次校验同一套白名单。

## 构建

准备 Android platform 35、build-tools 35.0.0 与 JDK,然后执行:

```bash
BUILD_MODE=debug ./build.sh
```

构建支持 `debug`、`unsigned` 和 `release`。正式签名只从环境变量读取 keystore
路径、alias 与密码,密钥和密码都不能进入 Git。可安装 APK 通过 GitHub
Release 发布,不提交进源码历史。

公开 Release 证书保存在
[`release-certificate.pem`](release-certificate.pem),证书 SHA-256 指纹为
`D0:2C:A4:E7:A4:FB:74:7A:E4:46:92:1A:DD:19:B1:1D:83:65:EE:3E:29:30:A4:CA:2B:25:94:AB:CE:74:70:45`。
后续 APK 除核对附件 SHA-256 外,也应核对这一签名身份。

## 手机端配置

1. 在 Termux 安装 Android AI Stack 操作脚本;
2. 理解外部命令边界后执行 `install-control-bridge --enable-external-apps`;
3. 安装 APK,并在 Android 提示时授予 `com.termux.permission.RUN_COMMAND`;
4. 打开 App,分别添加 Happy、llama.cpp、模型配置三个微件。

把 [`config/widget-profiles.example`](../../config/widget-profiles.example) 复制到
`~/.config/android-ai-stack/widget-profiles`,保持 mode `0600`,只填写脱敏的
Profile 名称和展示名。桥接层不会读取 Provider Token 或 CC Switch 数据库。

## 升级签名说明

使用 debug key 签名的旧版本不能直接覆盖升级到公开 release 签名版本。首次迁移
需要卸载 debug 版、安装 release 版并重新添加微件;之后的正式版本可以正常覆盖升级。
Loading
Loading