Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
73b4029
Merge remote-tracking branch 'upstream/master' into feat/desktop-linu…
CX33071 Nov 2, 2026
8e32966
fix(desktop): bound diagnostic registration reads
CX33071 Nov 3, 2026
cdf6f91
feat(desktop): integrate Linux personal service lifecycle
CX33071 Nov 4, 2026
d8e816d
fix(desktop): keep macOS Ops fixtures on private temp
CX33071 Nov 4, 2026
fe3b7de
Merge upstream/master into feat/desktop-linux-08e
CX33071 Nov 4, 2026
c254b1c
fix(desktop): complete Linux resource and session support
CX33071 Nov 5, 2026
911d6f8
style: format managed resource preparation
CX33071 Nov 5, 2026
4bd2a70
fix(desktop): preserve Linux XDG service environment
CX33071 Nov 5, 2026
2e168b1
test(resources): run case-sensitive path regression on Linux only
CX33071 Nov 5, 2026
6a85f82
Merge remote-tracking branch 'upstream/master' into feat/desktop-linu…
CX33071 Nov 6, 2026
861dacc
fix(desktop): bound macOS workflow diagnostics
CX33071 Nov 6, 2026
6b2f53b
fix(gateway): add missing license header
CX33071 Nov 6, 2026
e4e3dd9
test(desktop): validate optional Linux CI gates
CX33071 Nov 7, 2026
37e1e36
ci(desktop): isolate optional Linux candidate
CX33071 Nov 7, 2026
05f1a55
Merge upstream/master into feat/desktop-linux-08e
CX33071 Nov 7, 2026
24018fb
ci(desktop): fix managed resource matrix selection
CX33071 Nov 9, 2026
d52ec00
ci(desktop): isolate managed resource caller concurrency
CX33071 Nov 10, 2026
0d7484d
Merge upstream/master into feat/desktop-linux-08e
CX33071 Nov 11, 2026
dcf60c3
style: format CI pipeline test
CX33071 Nov 11, 2026
ed2a233
Merge upstream/master into feat/desktop-linux-08e
CX33071 Nov 12, 2026
824e95a
docs(desktop): clarify Linux service validation
CX33071 Nov 12, 2026
c5b9a19
fix(desktop): preserve Linux lifecycle and XDG boundaries
CX33071 Nov 13, 2026
03216c2
fix(ops): align Linux XDG fallback paths
CX33071 Nov 13, 2026
7289b3d
fix(desktop): align Linux XDG support paths
CX33071 Nov 13, 2026
eb0967d
fix(desktop): preserve Linux Ops support path
CX33071 Nov 13, 2026
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
6 changes: 5 additions & 1 deletion .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ jobs:
- run: pnpm build
- run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
- run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
- run: cargo test --locked --manifest-path src-tauri/Cargo.toml
- name: Test Windows IPC and Desktop behavior
run: cargo test --locked --manifest-path src-tauri/Cargo.toml
- run: pnpm ipc:check
- name: Verify diagnostic process containment
run: cargo run --locked --manifest-path src-tauri/Cargo.toml --example diagnostic_process_probe
Expand Down Expand Up @@ -244,6 +245,9 @@ jobs:
if: >-
${{ !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.installer_run == '') &&
((inputs.suites == '' && needs.changes.result != 'success') || fromJSON(inputs.suites || needs.changes.outputs.suites || '{}').desktop) }}
# Ubuntu Desktop qualification is optional under #2112; required Windows
# and macOS validation must remain independent of a Linux failure.
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 40
defaults:
Expand Down
103 changes: 92 additions & 11 deletions .github/workflows/managed-resource-candidates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,30 @@ on:
description: Scope already selected by the Main orchestrator
required: true
type: string
targets:
description: JSON array of resource targets to qualify; empty selects every target
required: false
type: string
default: ''
concurrency_suffix:
description: Suffix that isolates independent callers of this reusable workflow
required: false
type: string
default: default
workflow_dispatch:
inputs:
concurrency_suffix:
description: Suffix that isolates this manual run from other callers
required: false
type: string
default: manual

permissions:
actions: read
contents: read

concurrency:
group: managed-resource-candidates-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}
group: managed-resource-candidates-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }}-${{ inputs.concurrency_suffix || 'default' }}
cancel-in-progress: true

jobs:
Expand All @@ -24,21 +40,55 @@ jobs:
with:
baseline-workflow: managed-resource-candidates.yml

select-targets:
name: Select candidate targets
runs-on: ubuntu-24.04
outputs:
matrix: ${{ steps.select.outputs.matrix }}
steps:
- id: select
env:
TARGETS: ${{ inputs.targets }}
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json
import os

rows = [
{"target": "linux-x86_64", "runner": "ubuntu-24.04", "optional": True},
{"target": "windows-x86_64", "runner": "windows-2025", "optional": False},
{"target": "macos-arm64", "runner": "macos-15", "optional": False},
{"target": "macos-x86_64", "runner": "macos-15-intel", "optional": False},
]
requested = os.environ["TARGETS"].strip()
selected = set(json.loads(requested)) if requested else {row["target"] for row in rows}
if not selected:
raise SystemExit("at least one managed-resource target must be selected")
known = {row["target"] for row in rows}
unknown = selected - known
if unknown:
raise SystemExit(f"unsupported managed-resource target(s): {sorted(unknown)}")
print("matrix=" + json.dumps(
{"include": [row for row in rows if row["target"] in selected]},
separators=(",", ":"),
))
PY

candidate:
needs: changes
if: ${{ !cancelled() && ((inputs.suites == '' && needs.changes.result != 'success') || fromJSON(inputs.suites || needs.changes.outputs.suites || '{}').managed_resources) }}
needs: [changes, select-targets]
if: >-
${{ !cancelled() &&
((inputs.suites == '' && needs.changes.result != 'success') ||
fromJSON(inputs.suites || needs.changes.outputs.suites || '{}').managed_resources) }}
name: Candidate ${{ matrix.target }}
# Linux candidate qualification is optional for #2110. Keep its result and
# diagnostics visible without allowing it to fail the required Windows/macOS
# candidate aggregate.
continue-on-error: ${{ matrix.optional }}
strategy:
max-parallel: 2
fail-fast: false
matrix:
include:
- target: windows-x86_64
runner: windows-2025
- target: macos-arm64
runner: macos-15
- target: macos-x86_64
runner: macos-15-intel
matrix: ${{ fromJSON(needs.select-targets.outputs.matrix) }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 90
defaults:
Expand Down Expand Up @@ -91,6 +141,37 @@ jobs:
PY
- name: Produce twice, compare bytes, consume catalog, and run bundled launcher/Ops
run: uv run --no-sync pytest -q tests/test_managed_resource_producer.py tests/test_ops_resources.py --junitxml build/resource-tests.xml
- name: Install Linux Desktop dependencies for native Ops acceptance
if: matrix.target == 'linux-x86_64'
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y \
build-essential \
dbus-x11 \
gnome-keyring \
libayatana-appindicator3-dev \
libgtk-3-dev \
libsecret-1-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
librsvg2-dev \
libxdo-dev \
openjdk-17-jre-headless \
pkg-config
- name: Install pinned Rust toolchain for native Ops acceptance
if: matrix.target == 'linux-x86_64'
run: rustup toolchain install 1.95.0 --profile minimal
- name: Verify the real Linux Ops bundle through the native trust boundary
if: matrix.target == 'linux-x86_64'
run: |
mkdir -p build/resource-evidence
export POWERCONTEXT_OPS_TEST_BUNDLE="$GITHUB_WORKSPACE/build/resource-candidate/ops"
export POWERCONTEXT_OPS_TEST_BUNDLE_SHA256="$(sha256sum "$POWERCONTEXT_OPS_TEST_BUNDLE/bundle.json" | cut -d ' ' -f1)"
{
echo "bundle=$POWERCONTEXT_OPS_TEST_BUNDLE"
echo "bundle_manifest_sha256=$POWERCONTEXT_OPS_TEST_BUNDLE_SHA256"
CARGO_BUILD_JOBS=2 rustup run 1.95.0 cargo test --locked --manifest-path desktop/src-tauri/Cargo.toml --lib actual_independent_bundle_runs_through_native_trust_and_read_only_bridge -- --ignored
} 2>&1 | tee build/resource-evidence/native-ops-bridge.txt
- name: Retain unqualified candidate evidence
if: success()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,24 @@ jobs:
uses: ./.github/workflows/managed-resource-candidates.yml
with:
suites: ${{ needs.quality.outputs.suites }}
# Windows and macOS are the required managed-resource platforms. Linux
# qualification runs in a separate optional caller below.
targets: '["windows-x86_64","macos-arm64","macos-x86_64"]'
concurrency_suffix: required

managed-resource-candidates-linux:
needs: [quality, tests]
if: >-
${{ !cancelled() && needs.quality.result == 'success' &&
contains(fromJSON('["success","skipped"]'), needs.tests.result) &&
(fromJSON(needs.quality.outputs.suites || '{}').managed_resources) }}
uses: ./.github/workflows/managed-resource-candidates.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Give the required and optional callers separate concurrency groups

Both calls inherit managed-resource-candidates-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }} with cancel-in-progress: true, so they cancel each other despite the target split. On 24018fb0, the required selector job is already cancelled with the annotation Canceling since a higher priority waiting request for managed-resource-candidates-2471-pull_request exists; its candidate matrix is cancelled too. Please distinguish the two callers in the concurrency key so optional Linux cannot preempt the required Windows/macOS qualification.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this.
The required and optional callers now use separate concurrency groups. The required Windows/macOS caller passes concurrency_suffix: required, while the optional Linux caller passes concurrency_suffix: linux. The suffix is included in the reusable workflow’s concurrency key, so the Linux run can no longer cancel the required candidate run.
I also added regression assertions for the two caller-specific groups. The focused CI tests pass locally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 resolved at c5b9a19] The two callers now use distinct concurrency keys: the reusable group is managed-resource-candidates-<pr|ref>-<event>-<concurrency_suffix|default>, and master.yml passes concurrency_suffix: required for the windows/macos caller and concurrency_suffix: linux for the optional Linux caller. A Linux run can no longer cancel the required selection. Both callers still share github.event.pull_request.number || github.ref with cancel-in-progress: true, so a re-run of the same caller still supersedes its own earlier run, which is the intended behaviour.

with:
suites: ${{ needs.quality.outputs.suites }}
# Linux is optional under #2112. Its diagnostics and artifacts remain
# visible, but it is deliberately outside the required result gate.
targets: '["linux-x86_64"]'
concurrency_suffix: linux

script-installation:
needs: [quality, tests]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/native-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
linux-systemd-user:
needs: changes
if: ${{ !cancelled() && ((inputs.suites == '' && needs.changes.result != 'success') || fromJSON(inputs.suites || needs.changes.outputs.suites || '{}').native) }}
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
POWERCONTEXT_RUN_NATIVE_SERVICE_TESTS: "1"
Expand Down
10 changes: 10 additions & 0 deletions desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ This explicit probe creates a uniquely named synthetic credential in the preview

The Windows CI workflow runs on PRs and the preview branch, verifies native vault persistence, builds an unsigned internal installer, and tests Chinese-path installation/uninstallation on its disposable runner. It uploads the installer and a JSON smoke report. CI and mocked IPC tests do not prove clean-machine installation, notification activation, independent service login or Agent capture/recall.

## Local service status

On Linux, the Local Server settings page can inspect the PowerContext user service through the trusted Ops Bridge. This check is read-only: it reports supported, registration, ownership, manager, liveness, readiness and maintenance status, while keeping paths, commands, environment values and raw process output out of the UI.

The bridge only addresses PowerContext's registered service target and rejects untrusted windows, remote origins and arbitrary renderer input. A missing or unavailable local service does not disable remote Server connections. The page uses `ops.inspect` for read-only status inspection; with a valid Linux binding, explicitly approved plans dispatched through `ops.apply` can install, repair, start, stop or restart the personal service. These lifecycle operations retain the same ownership and approval rules.

The unit is written to the current user's `$XDG_CONFIG_HOME/systemd/user/` directory, or `~/.config/systemd/user/` when `XDG_CONFIG_HOME` is unset, and all manager calls use `systemctl --user`. Normal installation and recovery do not require `sudo`, never write a system unit under `/etc/systemd/system/`, and never call `loginctl enable-linger`. At login, the user's existing systemd manager may activate the enabled unit through `default.target`; at logout, the service follows that user manager's normal lifetime. If the account already has linger enabled by the user or administrator, PowerContext does not change that setting or silently create system-wide persistence.

Linux validation combines the Desktop Rust IPC/Ops Bridge tests with the real systemd `--user` lifecycle suite in the [native personal service workflow](../.github/workflows/native-service.yml). The lifecycle suite covers installation, restart, uninstall, foreign registrations, unrelated user services, recovery after an external stop, an unavailable user manager, and the user-session boundary. It verifies that login activation is the user's `default.target`, explicit stop leaves that activation enabled, the pre-existing linger state is unchanged, and no system unit is created. The suite does not force a logout of the CI account; actual GNOME session logout/login qualification belongs to the Ubuntu Desktop package acceptance. The managed-resource candidate workflow also generates a real Linux Ops bundle from a pinned standalone Python runtime and runs it through the native manifest/digest trust boundary; that candidate check does not claim signed release qualification.

## Local CLI diagnostics

Settings provides explicit local service and Agent integration checks. They remain separate from the active remote connection; a missing local CLI does not disable remote operations. Integration checks may start temporary Agent helpers and do not prove capture/recall.
Expand Down
10 changes: 10 additions & 0 deletions desktop/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ pnpm --dir desktop desktop:dev

[安全边界](SECURITY.md)记录具体约束;[验证说明](VALIDATION.md)列出测试范围和仍未满足的 P0 门槛。构建成功、Mock 测试和当前用户机器上的运行都不能代替标准用户、签名、通知冷启动激活、独立服务和 Agent 宿主验收。

## 本地服务状态

在 Linux 上,「本地 Server」设置页可以通过可信的 Ops Bridge 检查 PowerContext 的用户服务。这个检查是只读的:它只显示是否支持、是否注册、管理归属、管理器状态、存活状态、就绪状态和维护状态,不会把路径、命令、环境值或原始进程输出显示给界面。

Ops Bridge 只处理已登记的 PowerContext 服务目标,并拒绝不可信窗口、远程来源和渲染器传入的任意输入。本地服务缺失或暂时不可用时,远程 Server 连接仍然可以使用。页面通过 `ops.inspect` 执行只读状态检查;在有效的 Linux binding 下,经过明确批准并通过 `ops.apply` 派发的计划可以安装、修复、启动、停止或重启个人服务。这些生命周期操作仍然必须遵守相同的归属和审批规则。

服务单元写入当前用户的 `$XDG_CONFIG_HOME/systemd/user/`;未设置 `XDG_CONFIG_HOME` 时使用 `~/.config/systemd/user/`,所有管理器调用都使用 `systemctl --user`。正常安装和恢复不需要 `sudo`,不会写入 `/etc/systemd/system/` 之类的系统级 unit,也不会调用 `loginctl enable-linger`。用户登录时,已有的用户级 systemd manager 可以通过 `default.target` 激活已启用的服务;用户注销时,服务遵循该用户 manager 的正常生命周期。如果账户本来已经由用户或管理员启用了 linger,PowerContext 不会修改它,也不会默默创建系统级持久化。

Linux 验收由 Desktop Rust IPC/Ops Bridge 测试和真实 systemd `--user` 生命周期测试共同覆盖,后者位于 [native personal service workflow](../.github/workflows/native-service.yml)。生命周期测试覆盖安装、重启、卸载、外部登记、无关用户服务、外部停止后的恢复、用户服务管理器不可用,以及用户会话边界。测试验证登录激活使用当前用户的 `default.target`,显式停止不会取消该激活,已有 linger 状态保持不变,也不会创建系统级 unit。测试不会强制注销 CI 账户;真实 GNOME 会话的注销/重新登录资格由 Ubuntu Desktop 打包验收覆盖。managed-resource candidate workflow 还会使用固定 SHA-256 的独立 Python runtime 生成真实 Linux Ops bundle,并通过原生 manifest/摘要信任边界验收;该 candidate 检查不等同于已签名发布包资格。

## 本机 CLI 诊断

设置中的诊断只检查当前电脑,与远程连接状态分开。程序不从 PATH 自动选择 CLI,也不安装或启动 Server。集成检查可能运行临时 Agent 辅助进程;结果不代表实际 capture/recall 已验证。
Expand Down
17 changes: 17 additions & 0 deletions desktop/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,23 @@ Product qualification records remain unchanged. These isolated local processes d
external production deployments, managed installation/apply, installed Windows/macOS UI,
login/reboot, signing or notarization. UI tests mock IPC, and native unit HTTP fixtures are synthetic.

### Linux local-service validation

The Linux Desktop job runs the full Rust suite, including the Linux Ops Bridge and trusted-window IPC boundary tests. The separate [native personal service workflow](../.github/workflows/native-service.yml) runs the lifecycle suite against the runner's real systemd `--user` manager. It covers installation, start/restart, uninstall, foreign registrations, unrelated user services, recovery after an external stop, an unavailable user manager, and the user-session boundary. The native checks verify `default.target` login activation, explicit stop without disabling that activation, unchanged pre-existing linger state, and absence of a system-level unit. They do not force logout of the active CI account; GNOME logout/login behavior is reserved for installed Ubuntu Desktop qualification. The Linux managed-resource candidate job builds a real Ops bundle from a pinned standalone Python runtime and runs the generated bundle through the native manifest/digest trust test; the candidate remains unsigned and is not a release qualification.

To reproduce the systemd lifecycle checks on a disposable Linux user manager:

```sh
sudo systemctl start "user@$(id -u).service"
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"
export POWERCONTEXT_RUN_NATIVE_SERVICE_TESTS=1
export POWERCONTEXT_NATIVE_SERVICE_IDENTIFIER="powercontext-local-validation-$(id -u).service"
uv run --no-sync pytest -q tests/native/test_personal_service_lifecycle.py
```

The Desktop page uses `ops.inspect` for read-only status inspection and dispatches explicitly approved lifecycle plans through `ops.apply`; with a valid Linux binding, the page can install, repair, start, stop or restart the personal service. These operations use the current user's `systemctl --user` manager and do not enable linger, write system units or require root. A passing Rust, lifecycle or candidate-bundle test does not by itself qualify a signed package or a normal user's login-session configuration.

## Deployment context validation

On 2026-10-28, Windows 10 x64 / Python 3.11.15 / Rust 1.95.0 ran the deployment-identity
Expand Down
13 changes: 7 additions & 6 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,19 @@ pub fn run() {
.as_ref()
.map(|manager| manager.observations().clone())
.unwrap_or_default();
let home = app.path().home_dir().ok();
let local_data = ops_bridge::resolve_local_data_dir(
app.path().local_data_dir().ok(),
home.as_deref(),
);
app.manage(observations.clone());
app.manage(commands::HostState { manager });
app.manage(diagnostics::DiagnosticHost::new(
app.path().app_data_dir().ok(),
));
app.manage(
ops_bridge::OpsHost::new(
app.path().resource_dir().ok(),
app.path().local_data_dir().ok(),
app.path().home_dir().ok(),
)
.with_observations(observations),
ops_bridge::OpsHost::new(app.path().resource_dir().ok(), local_data, home)
.with_observations(observations),
);
let config = &app.config().app.windows[0];
tauri::WebviewWindowBuilder::from_config(app, config)?
Expand Down
31 changes: 30 additions & 1 deletion desktop/src-tauri/src/ops_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,35 @@ const APPROVAL_TTL: Duration = Duration::from_secs(600);
const MAX_APPROVALS: usize = 32;
pub const PROGRESS_EVENT: &str = "ops-progress";

pub(crate) fn resolve_local_data_dir(
local_data: Option<PathBuf>,
home: Option<&std::path::Path>,
) -> Option<PathBuf> {
#[cfg(target_os = "linux")]
{
return resolve_local_data_dir_from(
local_data,
home,
std::env::var_os("XDG_DATA_HOME").map(PathBuf::from),
);
}
#[cfg(not(target_os = "linux"))]
{
let _ = home;
local_data
}
}

#[cfg(target_os = "linux")]
fn resolve_local_data_dir_from(
local_data: Option<PathBuf>,
home: Option<&std::path::Path>,
configured_data_home: Option<PathBuf>,
) -> Option<PathBuf> {
let _ = local_data;
trust::trusted_linux_data_home_from(home, configured_data_home)
}

#[derive(Clone)]
pub struct OpsHost(Arc<Host>, Option<SessionObservations>);
struct Host {
Expand Down Expand Up @@ -527,5 +556,5 @@ fn request(action: OpsAction, mut fields: Value) -> Value {
fields["action"] = action.wire_name().into();
fields
}
#[cfg(all(test, target_os = "macos"))]
#[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
mod tests;
Loading
Loading