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
4 changes: 4 additions & 0 deletions .agents/skills/develop-maple-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ feed; publication is a separate protected operation. Invitations require the
platform-specific signed installed-update rehearsal. Linux uses user-owned tar
bundles and a separate v2 wire schema, preserving the shipped macOS v1 feed;
one release may update both platforms to the same version.
Windows x64 Dev/Prod installers are built by the same desktop workflow, with
master Authenticode signing through the existing `windows-signing` environment.
Windows uses manual installer downloads; automatic updates and built-in CUA
remain unsupported. Preserve that boundary when changing package metadata.
Dev and unpackaged builds have no production updater. Empty-key profiles still
disable update traffic. Do not substitute a test key or inherit feed/key overrides
from the launch environment. Embedded profiles, bundle IDs and state namespaces
Expand Down
328 changes: 328 additions & 0 deletions .github/workflows/agent-desktop-build.yml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/maple-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,9 @@ check.

## Update and release boundary

Automatic updates currently support macOS and Linux. Windows Dev/Prod installers
are Actions downloads with manual upgrades; see [desktop builds](docs/desktop-builds.md).

Packaged Prod builds use the independent signed Agent update feed. Dev and
unpackaged/MDE builds do not check a production updater. The Prod profile pins
the update URL and reviewed primary/backup public keys; inherited environment
Expand Down
14 changes: 11 additions & 3 deletions apps/maple-agent/app/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ fn main() {
source_sha,
} = git_metadata::emit();

// The CLI and macOS signing preflight read the same immutable public
// metadata. Signing must inspect the Mach-O section without running a
// downloaded executable while its keychain or Apple credentials exist.
// The CLI and native signing preflights read the same immutable public
// metadata. Signing must inspect the artifact without running a downloaded
// executable while signing credentials exist.
let channel = std::env::var("MAPLE_RELEASE_PROFILE").ok();
let pcr = std::env::var("VITE_OPEN_SECRET_PCR_ENVIRONMENT").ok();
let (channel, profile) = release_profile::select_profile(channel.as_deref(), pcr.as_deref())
Expand Down Expand Up @@ -69,4 +69,12 @@ fn main() {
println!("cargo:rustc-link-arg-bin=maple-agent=-Wl,-rpath,/usr/lib/swift");
println!("cargo:rustc-link-arg-bin=maple-agent=-Wl,-rpath,@executable_path/../Frameworks");
}
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows")
&& std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc")
&& std::env::var("CARGO_CFG_TARGET_ARCH").as_deref() == Ok("x86_64")
{
// #[used] retains the object symbol; /INCLUDE additionally roots it
// through MSVC release /OPT:REF and LTO. x64 C symbols are undecorated.
println!("cargo:rustc-link-arg-bin=maple-agent=/INCLUDE:MAPLE_AGENT_BUILD_INFO");
}
}
13 changes: 6 additions & 7 deletions apps/maple-agent/app/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ mod send_receipt_tests;
pub(crate) mod subscription;
#[cfg(test)]
mod subscription_tests;
#[cfg(windows)]
mod windows_spawn;

use crate::backend::{self, LocalBackend};
use maple_agent::agent::AgentServiceEvent;
Expand Down Expand Up @@ -600,13 +602,10 @@ fn spawn_host() -> Result<(), String> {
}
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
command.creation_flags(0x00000008 | 0x00000200 | 0x01000000); // detached, new group, break away from client job
}
let mut child = command
.spawn()
.map_err(|e| format!("Cannot start Maple host: {e}"))?;
let child = windows_spawn::spawn(&mut command);
#[cfg(not(windows))]
let child = command.spawn();
let mut child = child.map_err(|e| format!("Cannot start Maple host: {e}"))?;
// Reap on Unix while the parent remains alive; the host is not kill-on-drop.
std::thread::spawn(move || {
let _ = child.wait();
Expand Down
26 changes: 26 additions & 0 deletions apps/maple-agent/app/src/host/windows_spawn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Detach a host without overriding an enclosing Windows job's policy.

use std::io;
use std::os::windows::process::CommandExt;
use std::process::{Child, Command};

const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x01000000;
const ERROR_ACCESS_DENIED: i32 = 5;

pub(super) fn spawn(command: &mut Command) -> io::Result<Child> {
let detached = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
match command
.creation_flags(detached | CREATE_BREAKAWAY_FROM_JOB)
.spawn()
{
Err(error) if error.raw_os_error() == Some(ERROR_ACCESS_DENIED) => {
// A job can refuse breakaway even when ordinary child creation is
// allowed. Keep that job's restrictions and lifetime on the host;
// retrying without breakaway never relaxes the enclosing policy.
command.creation_flags(detached).spawn()
}
result => result,
}
}
1 change: 1 addition & 0 deletions apps/maple-agent/app/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fn value_for(profile: &str, baked: &str, configured: Option<String>) -> String {
#[used]
#[unsafe(no_mangle)]
#[cfg_attr(target_os = "macos", unsafe(link_section = "__TEXT,__maple_info"))]
#[cfg_attr(target_os = "windows", unsafe(link_section = ".maple"))]
static MAPLE_AGENT_BUILD_INFO: [u8; include_bytes!(concat!(env!("OUT_DIR"), "/build-info.json"))
.len()] = *include_bytes!(concat!(env!("OUT_DIR"), "/build-info.json"));

Expand Down
61 changes: 54 additions & 7 deletions apps/maple-agent/docs/desktop-builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
`Maple Agent Desktop Builds` builds both hosted profiles when Agent inputs change
on protected `master`, and always on a manual dispatch from `master`. It compares
the current source with the last successful ancestor run that actually completed
all four Dev/Prod macOS/Linux downloaded-package verifiers. Failed, in-flight and
all six Dev/Prod macOS/Linux/Windows downloaded-package verifiers. Failed, in-flight and
selector-only runs do not advance that baseline, so a later unrelated push still
builds outstanding Agent changes. Missing history, API failures or no verified
baseline in the last 100 successful master runs select a full build. PRs continue
Expand All @@ -18,12 +18,17 @@ with a bundled runtime and a normal application directory. CI checks each
downloaded bundle's command-line startup and detached host lifecycle in an
Ubuntu 24.04 container with no Nix installation and no network
access. Each master run completes independently; newer PR updates cancel older
previews.
previews. Windows x64 packages are per-user NSIS installers requiring Windows 10
version 1703 (build 15063) or later. This baseline supplies the OS `icuuc.dll`
imported by GPUI; see Microsoft's [Windows ICU version history](https://learn.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode--icu-).
Trusted master
packages use the existing Azure publisher to Authenticode-sign the application,
embedded uninstaller and final installer. PRs produce unsigned Dev previews.

| Profile | Application | macOS bundle ID | Configuration/data namespace | Update delivery |
| --- | --- | --- | --- | --- |
| Dev | Maple Work Dev | `cloud.opensecret.maple.agent.dev` | `maple-agent-dev` | Disabled; master Actions artifacts |
| Prod | Maple Work | `cloud.opensecret.maple.agent` | `maple-agent-prod` | Signed Agent feed with primary/backup pins; publication separately gated |
| Prod | Maple Work | `cloud.opensecret.maple.agent` | `maple-agent-prod` | macOS/Linux signed Agent feed; Windows manual installer; publication separately gated |

The binary embeds its profile, hosted API/client/billing settings, and PCR
environment. `--build-info` prints the public build identity as JSON without
Expand Down Expand Up @@ -51,6 +56,20 @@ credentials. Runtime metadata, duplicate
Swift runtime, and real GPUI window/first-render checks run only in credential-free
verification. GUI startup uses empty temporary state and does not sign in.

Windows also compiles without signing credentials, linking the CRT statically.
A fresh packaging runner reads the executable's dedicated read-only `.maple`
PE metadata section and checks profile/source without executing artifact code.
Master packaging reuses `windows-signing` and the existing Azure signing helpers;
it does not need another Azure app registration or certificate profile. Native
verifiers check installed payload identity, system-only DLL imports, Authenticode
signatures for master packages, CLI startup and isolated install/uninstall. These
checks do not establish interactive rendering, browser sign-in or tray behavior.
The host detaches from the client's console and process group. If an enclosing
Windows job forbids breakaway, the host retains that job's limits and lifetime;
terminating the enclosing job also terminates its host.
Built-in Windows computer-use remains tracked separately in
[issue #1295](https://github.com/MaplePrivacyLabs/Maple/issues/1295).

## Download and install

Open the latest successful `Maple Agent Desktop Builds` run with completed
Expand All @@ -61,17 +80,26 @@ Its artifacts include the profile, platform, and run ID in their names:
- `maple-agent-prod-macos-aarch64-RUN`
- `maple-agent-dev-linux-x86_64-RUN`
- `maple-agent-prod-linux-x86_64-RUN`
- `maple-agent-dev-windows-x86_64-RUN`
- `maple-agent-prod-windows-x86_64-RUN`

Artifact names stay stable when failed jobs are retried, so verification can
reuse a successful profile's package from an earlier attempt. A rerun producer
replaces only its own profile/platform artifact after packaging verification;
the replacement has a new artifact ID and download link. Every package must
still match the run's source commit and pass the same native verification.
Internal macOS binary artifacts use `macos-aarch64-prebuilt` in their names.
Internal binary artifacts use `macos-aarch64-prebuilt` or
`windows-x86_64-prebuilt` in their names.
They retain the same run/profile identity across retries, so a packaging-only
retry can reuse a successful compilation while that internal artifact is retained.

Final packages are retained for 30 days; internal prebuilt binaries for 7 days.
Windows packages contain an `-windows-x86_64-setup.exe` installer, public
build-info, package manifest and checksums. Run the installer as your desktop
user. Dev and Prod use distinct per-user installation directories, shortcuts and
uninstall entries, separate from Research. Uninstall removes installed program
files and integration while preserving account, settings and task data. Windows
upgrades currently use a new installer download; automatic updates are disabled.
macOS packages contain a DMG and application
archive; drag the chosen Agent app into Applications. Both Agent profiles can
coexist with Maple Research. Linux packages contain
Expand Down Expand Up @@ -172,6 +200,24 @@ continues to use the macOS feed. Dev has no update feed on either platform.
Both platforms retain one release version and source commit; separate feed
paths preserve compatibility with existing macOS clients.

Windows builds run on the pinned Windows Rust/MSVC environment in CI, with
`RUSTFLAGS='-D warnings -C target-feature=+crt-static'` set before fingerprinting
and compilation. `build-release.sh` selects the explicit x64 MSVC target so host
build tools keep their normal linkage, then stages the exact resulting executable.
From the component directory in Git Bash on Windows:

```sh
./scripts/build-release.sh dev
python3 scripts/windows-release.py package dev --unsigned
python3 scripts/windows-release.py verify dev dist/dev --unsigned --smoke
```

The package script verifies its pinned NSIS compiler download. Signed packaging
uses the same command without `--unsigned`, only in the trusted master job after
Azure login and the existing pinned Artifact Signing module setup. The signing
runner never executes downloaded app code. Install/uninstall smoke runs only on
a separate credential-free verifier.

For a produced Prod updater archive, compile the trusted data-only scanner and
validate the exact package directory (add `--unsigned` for PR packages):

Expand All @@ -198,13 +244,14 @@ The independent Agent publisher promotes only verified Prod artifacts into the
Agent R2 bucket. It creates no GitHub Release/tag and does not redeploy Research.
The client accepts only its signed Agent product/channel/target manifest, with
the primary and backup keys embedded in the Prod profile. Packaged Prod builds
check their platform's feed. Publication remains separately gated, and
on macOS/Linux check their platform's feed. Windows has no updater feed or
automatic update backend. Publication remains separately gated, and
invitations require a signed native update rehearsal on every required launch
platform. An empty-key profile
still disables updater traffic. See
[updater activation](updates.md).

No new app-signing environment or Apple credential migration is needed. Keep the
existing `desktop-signing` credentials and policies, Research updater key,
No new app-signing environment or credential migration is needed. Keep the
existing `desktop-signing` and `windows-signing` credentials and policies, Research updater key,
publication workflow and compatibility URLs intact. No Research install is
replaced, overwritten or migrated by Agent packaging or publication.
4 changes: 4 additions & 0 deletions apps/maple-agent/docs/release-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ makes no update requests. Active trust requires two distinct public keys; the
backup supports planned rotation and does not revoke a compromised accepted
primary.

Windows Dev/Prod installers use the same profile and state identities, but their
compiled update feed is absent. Windows upgrades currently require a new
installer download; the existing macOS/Linux feeds do not authorize Windows updates.

Dev and unpackaged builds contain no updater feed or keys. No runtime override
can opt either into production updating. `MAPLE_DISABLE_UPDATE_CHECK=1` remains
a hard off for every profile. Release tag prefixes still identify packaging
Expand Down
3 changes: 3 additions & 0 deletions apps/maple-agent/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ packaging-test:
python3 ./scripts/test_linux_install.py
python3 ./scripts/test_macos_release_dylibs.py
python3 ./scripts/test_macos_build_info.py
python3 ./scripts/test_windows_build_info.py
python3 ./scripts/test_windows_host_spawn.py
python3 ./scripts/test_windows_release.py
python3 ./scripts/test_smoke_macos_release.py
python3 ./scripts/test_smoke_linux_bundle_host.py

Expand Down
19 changes: 17 additions & 2 deletions apps/maple-agent/scripts/build-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ cd "$component"
# scripts receive certificates, notarization passwords, or updater keys.
unset APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_ID APPLE_ID_PASSWORD
unset APPLE_PASSWORD APPLE_TEAM_ID TAURI_SIGNING_PRIVATE_KEY TAURI_SIGNING_PRIVATE_KEY_PASSWORD
unset AZURE_CLIENT_ID AZURE_TENANT_ID AZURE_SUBSCRIPTION_ID
unset MAPLE_WINDOWS_ARTIFACT_SIGNING_ENDPOINT MAPLE_WINDOWS_ARTIFACT_SIGNING_ACCOUNT_NAME
unset MAPLE_WINDOWS_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME
export MAPLE_RELEASE_PROFILE="$1"
if [[ "$1" == dev ]]; then
export VITE_OPEN_SECRET_PCR_ENVIRONMENT=development
Expand All @@ -25,7 +28,19 @@ fi
# Populate all targets before the offline notice inventory on this unprivileged
# worker; signing workers consume the generated output without Cargo.
cargo fetch --locked
cargo build --release -p maple-agent-app --locked --timings
target_args=()
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) target_args=(--target x86_64-pc-windows-msvc) ;;
esac
# An explicit Windows target keeps +crt-static off host proc macros and build
# scripts. Stage only this build's executable at the shared prebuilt path.
cargo build --release -p maple-agent-app --locked --timings "${target_args[@]}"
if [[ ${#target_args[@]} != 0 ]]; then
mkdir -p target/release
cp target/x86_64-pc-windows-msvc/release/maple-agent.exe target/release/maple-agent.exe
fi
python3 "$component/scripts/generate-third-party-notices.py"
"$component/target/release/maple-agent" --build-info |
binary="$component/target/release/maple-agent"
case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) binary="${binary}.exe" ;; esac
"$binary" --build-info |
python3 -c 'import json,sys; info=json.load(sys.stdin); expected=sys.argv[1]; assert info["profile"] == expected, "incorrect release profile"' "$1"
Loading
Loading