Skip to content
Closed
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
76 changes: 76 additions & 0 deletions .github/workflows/platform-vault.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Platform accounts

on:
pull_request:
paths:
- 'crates/oauth-service/**'
- 'crates/shell/examples/connected-app-host.rs'
- 'crates/shell/examples/connected-install.rs'
- 'crates/shell/examples/connected_support/**'
- 'tools/connected-e2e/backend_login.py'
- 'tools/connected-e2e/webdriver_browser.py'
- 'tools/connected-e2e/native.py'
- 'tools/connected-e2e/backend-login/**'
- 'tools/backend-login-fixture.py'
- 'Cargo.toml'
- 'Cargo.lock'
- 'native-runtime.lock.json'
- 'runtime-patches.lock.json'
- 'tools/runtime-patches/**'
- '.github/workflows/platform-vault.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: platform-vault-${{ github.ref }}
cancel-in-progress: true

jobs:
windows:
runs-on: windows-2022
timeout-minutes: 55
env:
PYTHONUTF8: '1'
MAKEPAD_D3D11_WARP: '1'
steps:
- name: Preserve runtime patch bytes
run: git config --global core.autocrlf false
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- uses: dtolnay/rust-toolchain@stable
- run: python tools/setup.py
- uses: Swatinem/rust-cache@v2
with:
shared-key: embedded-browser-windows
cache-on-failure: true
- name: Actual Windows Credential Manager lifecycle
# A fresh synthetic profile on a disposable runner. Missing/unusable
# native credentials are a failure, never replaced by a file vault.
# This checks storage/revocation, not provider login or user approval.
run: cargo test --locked --release -p octosense-oauth-service --features host host_vault_acceptance::platform_vault_persists_across_reopen_without_plaintext_credentials -- --ignored --exact --nocapture
- name: Build real connected-app host and signed installer
run: |
cargo build --locked --release -p octosense-shell --features acceptance-fixtures --example connected-app-host --example connected-install
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cargo build --locked --release -p octosense-app-hub --bin hub
- name: Browser automation client only
# Use the runner's existing Edge browser. No downloaded browser engine,
# developer profile, provider account, or credential injection.
run: python -m pip install 'playwright==1.55.0'
- name: Actual synthetic backend browser and callback
run: |
$edge = Join-Path ${env:ProgramFiles(x86)} 'Microsoft/Edge/Application/msedge.exe'
if (-not (Test-Path $edge)) { throw 'The runner must already provide Edge' }
python tools/connected-e2e/backend_login.py --binary target/release/examples/connected-app-host.exe --installer target/release/examples/connected-install.exe --hub target/release/hub.exe --chrome "$edge" --out "$env:RUNNER_TEMP/backend-login"
- uses: actions/upload-artifact@v7
if: always()
with:
name: windows-backend-account-receipt
if-no-files-found: ignore
# Never upload the whole private directory: it contains OAuth URLs,
# fictional tokens and raw failure diagnostics.
path: ${{ runner.temp }}/backend-login/receipt.json
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ members = [
"crates/oauth-service",
"crates/markdown-editor",
"crates/browser-smoke",
"crates/host-api-smoke",
"crates/wasm-host",
"apps/reference",
"apps/news/host-service",
Expand Down
31 changes: 31 additions & 0 deletions crates/host-api-smoke/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "octosense-host-api-smoke"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false

# Android needs a library entry point. Reuse the exact desktop acceptance
# host; this package supplies no alternate API implementation or grant path.
[lib]
path = "../shell/examples/host-api-lab.rs"
test = false
bench = false
doctest = false

[[bin]]
name = "octosense-host-api-smoke"
path = "../shell/examples/host-api-lab.rs"

[features]
default = ["acceptance-fixtures"]
acceptance-fixtures = ["octosense-shell/acceptance-fixtures"]

[dependencies]
makepad-widgets.workspace = true
makepad-app-module.workspace = true
octosense-appstore.workspace = true
octosense-app-hub.workspace = true
octosense-app-policy.workspace = true
octosense-shell.workspace = true
serde_json.workspace = true
43 changes: 42 additions & 1 deletion crates/oauth-service/src/host_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,20 @@ fn method(
args,
result,
)
.with_platforms(&["macos", "android"])
// Native account/vault and external-browser routes exist on both desktop
// targets too. Do not hide read/local-draft methods from their agents.
// Write-review methods retain the physical-approval platform boundary;
// a separate OS-authenticated approval adapter must establish the others.
.with_platforms(
if matches!(
name,
"github.review_save" | "gcalendar.review_save" | "gmail.draft.review"
) {
&["macos", "android"]
} else {
&["macos", "android", "linux", "windows"]
},
)
.with_agent_access(agent)
}
fn result() -> Value {
Expand Down Expand Up @@ -326,6 +339,34 @@ pub(crate) fn gmail(native_review: bool) -> Vec<HostApiMethod> {
mod tests {
use super::*;
#[test]
fn desktop_accounts_reads_and_local_drafts_are_discoverable_without_widening_write_approval() {
let methods = auth()
.into_iter()
.chain(connector("github", true))
.chain(connector("gcalendar", true))
.chain(gmail(true));
let mut count = 0;
for method in methods {
let review = matches!(
method.name.as_str(),
"github.review_save" | "gcalendar.review_save" | "gmail.draft.review"
);
for platform in ["linux", "windows"] {
assert_eq!(
method.platforms.iter().any(|p| p == platform),
!review,
"{} on {platform}",
method.name
);
}
if method.name == "auth.backend.request" {
assert!(method.platforms.iter().any(|p| p == "linux"));
}
count += 1;
}
assert!(count > 20);
}
#[test]
fn descriptors_are_unique_valid_and_exclude_private_sheet_controls() {
let methods = auth()
.into_iter()
Expand Down
18 changes: 15 additions & 3 deletions crates/shell/examples/host-api-lab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,19 @@ struct App {

impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
#[cfg(not(target_os = "android"))]
let arg =
|name: &str| std::env::args().find_map(|a| a.strip_prefix(name).map(str::to_owned));
#[cfg(not(target_os = "android"))]
let root =
PathBuf::from(arg("--app-data=").expect("Pass an isolated --app-data directory"));
#[cfg(not(target_os = "android"))]
let bundle = PathBuf::from(arg("--bundle=").expect("Pass --bundle=<Host API Lab bundle>"));
#[cfg(target_os = "android")]
let data = PathBuf::from(cx.get_data_dir().expect("Android private data directory"))
.join("host-api-lab");
#[cfg(target_os = "android")]
let (root, bundle) = (data.join("apps"), data.join("bundle"));
let preview = std::env::args().any(|a| a == "--preview");
octosense_appstore::set_data_root(root.clone());
let policy = if preview {
Expand Down Expand Up @@ -117,9 +125,13 @@ impl MatchEvent for App {
if preview {
return;
}
self.receipt = Some(PathBuf::from(
#[cfg(not(target_os = "android"))]
let receipt = PathBuf::from(
arg("--receipt=").expect("Pass a new --receipt file"),
));
);
#[cfg(target_os = "android")]
let receipt = data.join("receipt.json");
self.receipt = Some(receipt);
assert!(
!self.receipt.as_ref().unwrap().exists(),
"Receipt must be a new file"
Expand Down Expand Up @@ -206,7 +218,7 @@ impl AppMain for App {
"signed_install":true,"bundle_digest":self.launch.as_ref().unwrap().manifest.integrity.bundle_blake3,
"tool_result":reply,"refusals":self.preliminary,"closed_app":closed,
"host_sheet_visible":self.sheet.borrow().is_some_and(|s| s.view.visible),
"not_verified":["model or peer relay consent", "physical OS permission approval", "camera capture", "Android runtime"]});
"not_verified":["model or peer relay consent", "physical OS permission approval", "camera capture"]});
use std::io::Write;
std::fs::OpenOptions::new()
.write(true)
Expand Down
2 changes: 2 additions & 0 deletions tools/ci_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
# Workflows ci-local deliberately does not run, and why. --check-drift skips
# them (an entry naming no workflow, or one ci-local runs, is drift).
NOT_LOCAL = {
"platform-vault.yml": "actual Windows Credential Manager and backend browser acceptance need a Windows session; "
"compilation or another OS is not native account evidence",
"embedded-browser.yml": "native Windows WebView2 acceptance needs a Windows desktop and installed engine; "
"ci-local-merge requires a successful GitHub run on the exact PR head",
"release-desktop.yml": "a release workflow (tag push or manual run): signed packages for three OSes, "
Expand Down
52 changes: 52 additions & 0 deletions tools/connected-e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,55 @@ copy. No provider write occurs. Normal Notes admission/provider and soak drivers
remain the commands above.

Android developer-backend login has its own isolated package and real-form procedure: [Android backend acceptance](ANDROID-BACKEND.md). It does not authorize Google embedded login.

## Native backend browser acceptance (2026-10-08)

`backend_login.py` uses a real host, browser and operating-system credential
vault. Only the backend HTTP server and its fictional users are synthetic; no
account or token is injected into the host. Linux and Windows use the supported
external-browser route with an ephemeral loopback callback. Embedded WebReader
acceptance is a separate test and does not establish backend sign-in.

The [Linux receipt](evidence/linux-backend-native.json) records seven successful
behaviors: browser registration/sign-in and PKCE callback, protected identity,
rotated refresh recovery, native restart, logout, repeat login and isolation
between two signed apps. MiniBrowser/WebKitGTK 2.52.6 and native Secret Service
ran in an isolated display/session; all owned processes and fictional
connections were cleaned up. The receipt binds the actual binary and source
hashes. The native examples were rebuilt from `e9765575` with its current locked
dependencies; all 36 recorded host/OAuth/fixture/lockfile hashes match the
reviewed source. The final run includes the responsive fixture and bounded
failure diagnostics; those two test files changed after the native build, with
no Rust source changes. This validates the installed-app fixture, not the entire
shell or OS-authenticated business writes.

Build the host/installer as above and the pinned Hub CLI with
`cargo build --locked --release -p octosense-app-hub --bin hub`. With an existing
isolated WebKitWebDriver, invoke `backend_login.py` with `--binary`, `--installer`,
`--hub`, `--webdriver http://127.0.0.1:PORT` and a new private `--out` directory.
The Linux run used that interface with the real engine and normal host vault.
`--chrome PATH` instead uses an existing Chromium browser through Playwright.
The `Platform accounts` workflow exercises the Windows native vault and this
same browser/callback journey using the runner's existing Edge installation.
The vault job passed; the added Windows browser journey is pending execution.

The acceptance host copies its own consent-sheet link into a private test
file, and the harness opens that exact URL in a fresh browser. **The OS default
browser link click is not tested.** Synthetic Makepad clicks cannot approve a
business write; these tests do not prove a physical press or OS-authenticated
approval. The disposable fictional account is not Google/GitHub provider
acceptance. Keep the entire run directory private: raw callback URLs and
fictional tokens are not suitable for public artifacts. Publish only reviewed
receipts.

The Linux test did not install packages systemwide. Its disposable bwrap mount
namespace overlays the owned extracted browser/vault package trees onto `/usr`,
read-only. Therefore WebDriver's MiniBrowser path resolves to the extracted
engine, not another system browser. The receipt includes both engine hashes.
Linux/Windows account, read and local-draft methods are now advertised in runtime
discovery; native write-review methods retain their physical-approval platform
limits. `auth.backend.request` supports declared GETs there, while mutations
still require the separately available native approval. Google authorization on
Android remains unsupported by the current provider adapter; the OnePlus 6 test
device has no Play Services. A method's platform support does not configure an
OAuth client or approve a remote write.
40 changes: 40 additions & 0 deletions tools/connected-e2e/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,43 @@ python3 tools/connected-e2e/notes_recovery.py \
测试不执行提供商写入;正常准入、提供商流程和浸泡测试继续使用上面的命令。

Android 开发者后端登录使用独立测试包和真实表单,参见 [Android 后端验收](ANDROID-BACKEND.zh-CN.md)。这不代表支持 Google 嵌入式登录。

## 原生后端浏览器验收(2026-10-08)

`backend_login.py` 使用真实宿主、浏览器和操作系统凭据库。只有后端 HTTP
服务及虚构用户是模拟数据;不会向宿主注入账户或 token。Linux 和 Windows
使用受支持的外部浏览器路径与临时回环回调。嵌入式 WebReader 的验收是另一个
测试,不能证明后端登录可用。

[Linux 回执](evidence/linux-backend-native.json) 记录七项成功行为:浏览器注册/
登录与 PKCE 回调、受保护身份读取、轮换刷新恢复、原生进程重启、退出登录、
再次登录,以及两个签名应用之间的账户隔离。MiniBrowser/WebKitGTK 2.52.6
和原生 Secret Service 在隔离显示/会话中运行;所有测试进程与虚构连接均已清理。
回执绑定实际二进制及源码哈希。原生示例使用 `e9765575` 的当前锁定依赖重新构建,
记录的全部 36 个宿主/OAuth/测试/锁文件哈希均与审阅源码一致。最终运行包括响应式
测试界面及有界故障诊断;这两个测试文件在原生构建后修改,Rust 源码没有变化。
这验证已安装应用测试流程,不代表整个 shell 或 OS 认证业务写入通过。

按前文构建宿主/安装器,并用
`cargo build --locked --release -p octosense-app-hub --bin hub` 构建固定版本 Hub
命令行。启动已有的隔离 WebKitWebDriver 后,以 `--binary`、`--installer`、
`--hub`、`--webdriver http://127.0.0.1:PORT` 和新的私有 `--out` 目录调用
`backend_login.py`。Linux 验收使用此接口、真实引擎及普通宿主凭据库。
`--chrome PATH` 则通过 Playwright 使用已有 Chromium 浏览器。
`Platform accounts` 工作流使用执行器已有的 Edge,验证 Windows 原生凭据库
及同一浏览器/回调流程。凭据库测试已通过,新增浏览器流程仍待执行。

验收宿主把其原生授权页中的链接复制到私有测试文件,测试器在新浏览器中打开
完全相同的 URL。**未测试操作系统默认浏览器链接的点击。** Makepad 合成点击
不能批准业务写入;这些测试不证明物理点击或操作系统身份认证批准。一次性
虚构账户也不等同于 Google/GitHub 提供商验收。整个运行目录须保密:原始回调
URL 及虚构 token 不适合作为公开工件,只发布审查过的回执。

Linux 测试未安装系统级软件包。一次性 bwrap 挂载命名空间把测试拥有的浏览器/
凭据库软件包目录只读叠加到 `/usr`;因此 WebDriver 的 MiniBrowser 路径解析到
提取出的引擎,而不是另一个系统浏览器。回执包含浏览器与 WebKit 库的哈希。
运行时发现现在会列出 Linux/Windows 的账户、读取和本地草稿方法;原生写入
审查方法仍保留物理批准的平台限制。`auth.backend.request` 在这些平台支持声明
的 GET,变更仍须可用的原生批准。当前提供商适配器仍不支持 Android Google
授权,OnePlus 6 测试设备没有 Play Services。方法支持某个平台不表示已经配置
OAuth 客户端,更不等于批准远端写入。
4 changes: 3 additions & 1 deletion tools/connected-e2e/backend-login/main.splash
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ SolidView {width: Fill height: Fill flow: Down padding: 24 spacing: 10 draw_bg.c
Label {width: Fill text: "Synthetic test account. Credentials stay in the browser and host vault." draw_text.color: #43536c}
account := Label {width: Fill text: "No account" draw_text.color: #172033}
identity := Label {width: Fill text: "No protected data" draw_text.color: #172033}
status := Label {width: Fill text: "Ready" draw_text.color: #233854}
ScrollYView {width: Fill height: Fill flow: Down spacing: 10
Button {width: Fill height: 44 text: "Connect backend" on_click: || connect("backend", ["app.session"])}
Button {width: Fill height: 44 text: "Connect in WebView" on_click: || connect_webview()}
Button {width: Fill height: 44 text: "Connect GitHub" on_click: || connect("github", ["read:user"])}
Expand All @@ -61,5 +63,5 @@ SolidView {width: Fill height: Fill flow: Down padding: 24 spacing: 10 draw_bg.c
Button {width: Fill height: 44 text: "Protected identity" on_click: || identity()}
Button {width: Fill height: 44 text: "Disconnect" on_click: || disconnect()}
Button {width: Fill height: 44 text: "Probe other app" on_click: || probe()}
status := Label {width: Fill text: "Ready" draw_text.color: #233854}
}
}
Loading
Loading