Skip to content

Commit c3d335a

Browse files
committed
feat(mobile): add Android companion and debug validation CI
Introduce an isolated Tauri HTTPS remote companion with a ten-locale launcher, strict origin/transport and launcher-only native authority. Include the shared iOS source-owned Int32 ABI fix with upstream licenses, immutable provenance and pre-link integrity guards; no linked or device-qualified iOS app is claimed. Harden preview HTTP/WebSocket credential and upstream TLS boundaries, retain no-reuse Undici containment and document residual dependencies. Add hosted mobile auth/upload, URL/authority and real loopback WebSocket/browser regressions. Apply targeted compatible npm remediation without changing desktop native sources. Validate PR source, Java XML, launcher browser and Rust policy separately from an explicit SDK-license-gated ARM64 debug APK job. Pin actions, use read-only tokens, preserve no secrets/release/store paths and upload only short-lived test APK/hash artifacts. Exclude generated API copies and the environment-bound historical Mac probe without deleting them. Integrate on current dev rather than carrying unrelated historical commits. Adapt the preview fixture to the current Config/I18n providers and normalize workflow test line endings for Windows checkouts. Local source19/19, launcher browser, ABI142/13, policy4/4 and security regressions pass; GitHub build and actual device acceptance remain distinct pending evidence.
1 parent 86dd9e8 commit c3d335a

272 files changed

Lines changed: 59869 additions & 787 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
name: Mobile Android validation and debug APK
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- '.github/workflows/mobile-android.yml'
7+
- 'packages/mobile-app/**'
8+
- 'packages/*/package.json'
9+
- 'packages/ui/src/styles/tokens.css'
10+
- 'packages/ui/src/lib/i18n/messages/**/remoteAccess.ts'
11+
- 'packages/server/src/server/routes/auth-pages/login.html'
12+
- 'package.json'
13+
- 'package-lock.json'
14+
- '.npmrc'
15+
workflow_dispatch:
16+
inputs:
17+
accept_sdk_licenses:
18+
description: I am authorized to accept Android SDK licenses on this disposable runner
19+
type: boolean
20+
required: true
21+
default: false
22+
23+
permissions:
24+
contents: read
25+
26+
concurrency:
27+
group: mobile-android-${{ github.event_name }}-${{ github.ref }}
28+
cancel-in-progress: true
29+
30+
jobs:
31+
source-validation:
32+
name: Launcher, source contracts and Rust policy (not a native APK)
33+
runs-on: windows-2022
34+
timeout-minutes: 25
35+
defaults:
36+
run:
37+
shell: pwsh
38+
steps:
39+
# github.sha is the PR merge commit, not an interpolated fork branch name.
40+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
41+
with:
42+
ref: ${{ github.sha }}
43+
persist-credentials: false
44+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
45+
with:
46+
node-version: '24'
47+
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
48+
with:
49+
distribution: temurin
50+
java-version: '17'
51+
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # reviewed master commit
52+
with:
53+
toolchain: '1.94.0'
54+
- name: Install locked npm dependencies without desktop lifecycle scripts
55+
run: npm ci --ignore-scripts
56+
- name: Require production Android XML parsing (no CI skip)
57+
working-directory: packages/mobile-app
58+
run: java --source 17 tests/native/TransportPolicyTest.java
59+
- name: Validate launcher and source-owned native contracts
60+
run: |
61+
npm run typecheck:mobile
62+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
63+
npm run ios:verify --workspace @codenomad/mobile-app
64+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
65+
npm run test:mobile
66+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
67+
npx --workspace @codenomad/mobile-app playwright install chromium
68+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
69+
npm run test:browser --workspace @codenomad/mobile-app
70+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
71+
cargo test --locked --manifest-path packages/mobile-app/src-tauri/policy/Cargo.toml
72+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
73+
- name: Report APK license gate separately from source validation
74+
if: always()
75+
env:
76+
SDK_LICENSE_AUTHORIZED: ${{ (github.event_name == 'pull_request' && vars.CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES == 'true') || (github.event_name == 'workflow_dispatch' && inputs.accept_sdk_licenses == true) }}
77+
run: |
78+
'Source/browser/policy checks are not Android compilation or device qualification.' >> $env:GITHUB_STEP_SUMMARY
79+
if ($env:SDK_LICENSE_AUTHORIZED -eq 'true') {
80+
'SDK license consent is enabled. See the separate ARM64 debug APK job for actual build success/failure; it also requires source validation to pass.' >> $env:GITHUB_STEP_SUMMARY
81+
} else {
82+
'ARM64 debug APK job SKIPPED: no explicit SDK license consent. An authorized maintainer must set CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES=true for PR builds, or confirm the manual dispatch checkbox. Source success is not APK build success.' >> $env:GITHUB_STEP_SUMMARY
83+
}
84+
debug-apk:
85+
name: ARM64 debug APK (explicit SDK license consent required)
86+
needs: source-validation
87+
if: ${{ (github.event_name == 'pull_request' && vars.CODENOMAD_ANDROID_ACCEPT_SDK_LICENSES == 'true') || (github.event_name == 'workflow_dispatch' && inputs.accept_sdk_licenses == true) }}
88+
runs-on: windows-2022
89+
timeout-minutes: 60
90+
defaults:
91+
run:
92+
shell: pwsh
93+
env:
94+
CI: 'true'
95+
NDK_VERSION: '28.2.13676358'
96+
steps:
97+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
98+
with:
99+
ref: ${{ github.sha }}
100+
persist-credentials: false
101+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
102+
with:
103+
node-version: '24'
104+
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
105+
with:
106+
distribution: temurin
107+
java-version: '17'
108+
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # reviewed master commit
109+
with:
110+
toolchain: '1.94.0'
111+
targets: aarch64-linux-android
112+
- name: Set up explicit Android SDK packages on the CI runner only
113+
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
114+
with:
115+
cmdline-tools-version: '12266719'
116+
accept-android-sdk-licenses: 'true'
117+
log-accepted-android-sdk-licenses: 'true'
118+
# CLI 2.12.1 generates API 37; the recovery library uses API 36.
119+
packages: platform-tools platforms;android-37.0 platforms;android-36 build-tools;36.0.0 ndk;28.2.13676358
120+
- name: Configure and verify execution-host Android paths
121+
run: |
122+
if (!$env:ANDROID_HOME -or !$env:JAVA_HOME) { throw 'SDK/JDK environment is missing.' }
123+
$ndk = Join-Path $env:ANDROID_HOME "ndk\$env:NDK_VERSION"
124+
foreach ($relative in @('platforms\android-37.0\android.jar', 'platforms\android-36\android.jar', 'build-tools\36.0.0\aapt.exe')) {
125+
if (!(Test-Path (Join-Path $env:ANDROID_HOME $relative))) { throw "Missing SDK component: $relative" }
126+
}
127+
if (!(Test-Path (Join-Path $ndk 'source.properties'))) { throw 'Pinned NDK is missing.' }
128+
"ANDROID_SDK_ROOT=$env:ANDROID_HOME" >> $env:GITHUB_ENV
129+
"NDK_HOME=$ndk" >> $env:GITHUB_ENV
130+
java -version
131+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
132+
rustc --version
133+
if ($LASTEXITCODE) { exit $LASTEXITCODE }
134+
- name: Install locked npm dependencies without desktop lifecycle scripts
135+
run: npm ci --ignore-scripts
136+
- name: Initialize Android without interactive prompts or extra Rust targets
137+
run: npm run android:init --workspace @codenomad/mobile-app -- --ci --skip-targets-install
138+
- name: Build development-signed ARM64 debug APK only
139+
run: npm run android:debug --workspace @codenomad/mobile-app -- --ci
140+
- name: Require APK output and create SHA-256 manifest
141+
run: |
142+
$outputs = Join-Path $env:GITHUB_WORKSPACE 'packages\mobile-app\src-tauri\gen\android\app\build\outputs\apk'
143+
if (!(Test-Path $outputs)) { throw 'Android APK output directory is missing.' }
144+
$apks = @(Get-ChildItem $outputs -Recurse -File -Filter '*debug*.apk')
145+
if ($apks.Count -eq 0) { throw 'Build completed without a debug APK.' }
146+
$destination = Join-Path $env:RUNNER_TEMP 'codenomad-mobile-apk'
147+
New-Item -ItemType Directory -Force $destination | Out-Null
148+
$hashes = foreach ($apk in $apks) {
149+
$target = Join-Path $destination $apk.Name
150+
if (Test-Path $target) { throw "Duplicate APK filename: $($apk.Name)" }
151+
Copy-Item $apk.FullName $target
152+
$hash = (Get-FileHash $target -Algorithm SHA256).Hash.ToLowerInvariant()
153+
"$hash $($apk.Name)"
154+
}
155+
$hashes | Set-Content (Join-Path $destination 'SHA256SUMS.txt') -Encoding utf8
156+
@("commit=$env:GITHUB_SHA", "run=$env:GITHUB_RUN_ID", 'variant=debug', 'target=aarch64', 'not-for-store-distribution=true') |
157+
Set-Content (Join-Path $destination 'build.txt') -Encoding utf8
158+
'## ARM64 debug APK SHA-256' >> $env:GITHUB_STEP_SUMMARY
159+
$hashes | ForEach-Object { "- ``$_``" >> $env:GITHUB_STEP_SUMMARY }
160+
- name: Upload test APK and hashes, not a store/release publication
161+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
162+
with:
163+
name: codenomad-android-debug-arm64-${{ github.run_id }}-${{ github.run_attempt }}
164+
path: ${{ runner.temp }}/codenomad-mobile-apk/
165+
if-no-files-found: error
166+
retention-days: 7
167+
compression-level: 0

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ Behavior for agents:
130130
- Run them with `npm run test:browser --workspace @codenomad/ui` after `npx playwright install chromium`. `CODENOMAD_BROWSER_PATH` optionally selects an existing Chromium executable; it does not target the installed application or user sessions.
131131

132132
## V2 Runtime Launch
133+
- The Android-first companion is isolated in `packages/mobile-app/`: one webview, a trusted bundled launcher, and a top-level hosted web/remote UI. Never bundle the desktop server/Node/CLI or grant remote native capabilities. Keep endpoint validation mirrored by shared JS/Rust fixtures; native navigation restricts redirects to the selected HTTPS origin. Recovery chrome belongs to the native mobile recovery plugin, never hosted DOM/IPC or a second mobile webview. Launcher-only styles reuse UI tokens in `packages/mobile-app/src/launcher.css`; its standalone locale adapter reuses each UI locale's remote-access messages without mounting server-backed preferences. All platform recovery changes require real-device verification before claiming mobile readiness.
133134
- Native automation instrumentation starts automatically; no Developer Mode toggle or activation restart is required. Do not configure a fixed CDP port or a manual WebView2 profile.
134135
- Rebuild Electron before calling `codenomad.act({ action: "restart" })`. For Windows Tauri, stop and relaunch the release executable only when the linker cannot replace it; never stop the shared OpenCode daemon.
135136

‎dev-docs/MOBILE_DELIVERY.md‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Mobile delivery gates
2+
3+
## Architecture decision
4+
5+
The Android-first companion is a separate Tauri v2 host, not a mobile build of
6+
the desktop process supervisor. It displays the selected CodeNomad server's
7+
hosted UI as a top-level page. Node, Git, OpenCode, directory authorization,
8+
session environment writes, and all execution remain on that server.
9+
10+
Same-origin hosting preserves the existing login cookie, CodeNomad API,
11+
allowlisted OpenCode proxy, and multiplexed SSE transport. A bundled copy of
12+
the transcript UI with cross-origin APIs is deliberately out of scope.
13+
14+
Only the bundled connection screen may manage endpoints. Remote content must
15+
have no native capabilities. A JavaScript host override selects web/remote UI
16+
behavior; this is not a replacement for native capability/origin enforcement.
17+
Endpoints require device-trusted HTTPS. There is no certificate bypass or
18+
cleartext fallback. The initial deployment is a controlled pilot, not a claim
19+
of unrestricted public-Internet or app-store readiness.
20+
21+
## Validation levels
22+
23+
Keep these outcomes separate in release reports:
24+
25+
1. Source and deterministic policy tests pass.
26+
2. Launcher build, shared UI typecheck, and browser fixtures pass.
27+
3. Rust/Tauri host compiles on a development host.
28+
4. Android APK compiles with an actual Android SDK/NDK and target.
29+
5. Emulator/physical-device acceptance checks pass.
30+
6. Signed distribution build and store requirements are satisfied.
31+
32+
A Chromium mobile viewport is not Android WebView or WKWebView validation.
33+
A Windows host compile is not an Android compile. A workflow definition is
34+
not evidence that the workflow ran or produced an installable APK.
35+
36+
## Android acceptance checklist
37+
38+
- Install a debug APK; confirm no Node/server/CLI resources are included.
39+
- Connect to a disposable authenticated CodeNomad fixture with trusted HTTPS.
40+
- Reject HTTP, credentials in URLs, unsupported URL components, and launcher
41+
origin collisions. Fence cross-origin navigation, redirects, and popups.
42+
- Prove remote pages and their frames cannot invoke launcher/native commands.
43+
- Prove invalid, expired, and hostname-mismatched TLS certificates fail closed.
44+
- Exercise native return/disconnect while loading, offline, and on TLS errors.
45+
- Log in, stream a response, settle Forms/permissions, and upload a device file.
46+
- Suspend and resume while work finishes; reconcile authoritative state and
47+
never replay failed prompt mutations on reconnection or reauthentication.
48+
- Restart the test backend and recover authentication without losing an
49+
in-memory draft or submitting it twice.
50+
- Check IME resize, rotation, safe areas, touch/nested scroll, RTL, and upload
51+
cancellation on the actual webview.
52+
- Kill and relaunch the app. Record cookie behavior and document that remote
53+
web mode does not inherit native desktop draft/tab restoration.
54+
55+
Use isolated fixture backends and databases, never the shared user daemon.
56+
57+
## iOS follow-up
58+
59+
Tauri supports iOS, but compilation requires macOS and full Xcode, CocoaPods,
60+
and iOS Rust targets. An installed macOS Rust target on Windows is insufficient.
61+
Repeat the acceptance checklist on WKWebView, including safe areas, keyboard,
62+
file selection, authentication persistence, and suspension. Signing and App
63+
Store distribution require an Apple team, certificate, and provisioning.
64+
65+
## Server exposure prerequisites
66+
67+
The architecture audit of Git HEAD `9597853e` identified separate hardening
68+
work before claiming unrestricted Internet readiness:
69+
70+
- Session expiry, bounded storage, revocation on logout/password changes.
71+
- Credentialed CORS and request-origin/CSRF policy.
72+
- Login abuse protection.
73+
- Explicit reverse-proxy TLS trust and Secure-cookie behavior.
74+
75+
Mobile login is full backend access, not a read-only observer role. Hiding
76+
administrative controls does not create an authorization boundary. A VPN
77+
reduces exposure but does not turn these open hardening items into completed
78+
security work.
79+
80+
## Official references
81+
82+
- https://v2.tauri.app/start/prerequisites/
83+
- https://v2.tauri.app/start/project-structure/
84+
- https://v2.tauri.app/security/capabilities/
85+
- https://v2.tauri.app/distribute/google-play/
86+
- https://v2.tauri.app/distribute/sign/android/
87+
- https://v2.tauri.app/distribute/sign/ios/
88+
- https://v2.tauri.app/distribute/app-store/

0 commit comments

Comments
 (0)