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
8 changes: 5 additions & 3 deletions .agents/skills/develop-maple-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,11 @@ ownership at the layer implementing the effect. Never treat a passing source
import or a native login screen as authenticated chat or containment proof.

Packaged Agent Prod uses its independently signed R2 update feed with compiled
primary and backup public-key pins. Packaged Prod checks the compiled feed;
publication remains disabled pending owner service setup. Invitations still
require the signed installed-update rehearsal.
primary and backup public-key pins. Packaged Prod checks the compiled platform
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.
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
57 changes: 44 additions & 13 deletions .github/workflows/agent-desktop-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ jobs:
retention-days: 30

linux:
name: Linux AppImage (${{ matrix.variant }})
name: Linux bundle (${{ matrix.variant }})
needs: changes
if: ${{ always() && !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.agent != 'false') && (github.event_name == 'pull_request' || (github.repository == 'MaplePrivacyLabs/Maple' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch'))) }}
runs-on: blacksmith-8vcpu-ubuntu-2404
Expand Down Expand Up @@ -311,7 +311,7 @@ jobs:
cp "$RUNNER_TEMP/agent-environment.json" target/ci-metrics/environment.json
- name: Build optimized Agent with embedded profile
run: nix develop --no-update-lock-file . -c python3 ../../scripts/ci/agent_measure.py --phase release-build --target target -- ./scripts/build-release.sh "$MAPLE_RELEASE_PROFILE"
- name: Package Linux AppImage
- name: Package Linux installed bundle
run: |
args=()
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then args=(--unsigned); fi
Expand All @@ -320,7 +320,7 @@ jobs:
run: |
args=()
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then args=(--unsigned); fi
nix develop --no-update-lock-file . -c ./scripts/verify-release.sh "$MAPLE_RELEASE_PROFILE" "dist/$MAPLE_RELEASE_PROFILE" "${args[@]}"
nix develop --no-update-lock-file . -c ./scripts/verify-release.sh "$MAPLE_RELEASE_PROFILE" "dist/$MAPLE_RELEASE_PROFILE" "${args[@]}" --platform linux-x86_64
- name: Upload Agent packages
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
Expand Down Expand Up @@ -351,16 +351,36 @@ jobs:
id-token: write
attestations: write
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
variant: [dev, prod]
env:
MAPLE_RELEASE_PROFILE: ${{ matrix.variant }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Install pinned Nix
uses: DeterminateSystems/determinate-nix-action@96c5ee93a9dddf2830d31e1da6ec719077336c28 # v3.23.3
with:
source-tag: v3.23.3
github-token: ""
- name: Verify pinned Nix runtime
working-directory: .
run: bash scripts/ci/verify-nix-runtime.sh
- name: Build trusted installer archive scanner
run: nix develop --no-update-lock-file . -c cargo build --locked -p maple-agent-updater --example validate-release-archive
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
name: maple-agent-${{ matrix.variant }}-linux-x86_64-${{ github.run_id }}
path: artifacts
- name: Validate exact Linux artifact inventory and identity
run: python3 scripts/release-info.py verify "$MAPLE_RELEASE_PROFILE" ../../artifacts --source-sha "$GITHUB_SHA" --platform linux-x86_64
- name: Scan actual Linux bundle with installer parser
run: nix develop --no-update-lock-file . -c ./scripts/verify-updater-archive.sh ../../artifacts --profile "$MAPLE_RELEASE_PROFILE" --platform linux-x86_64
- name: Attest exact downloadable Linux packages
uses: actions/attest@281a49d4cbb0a72c9575a50d18f6deb515a11deb
with:
Expand Down Expand Up @@ -472,33 +492,44 @@ jobs:
done
env:
GH_TOKEN: ${{ github.token }}
- name: Build trusted installer archive scanner
run: nix develop --no-update-lock-file . -c cargo build --locked -p maple-agent-updater --example validate-release-archive
- name: Scan downloaded Linux bundle with installer parser
run: |
args=()
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then args=(--unsigned); fi
nix develop --no-update-lock-file . -c ./scripts/verify-updater-archive.sh "../../artifacts" --profile "$MAPLE_RELEASE_PROFILE" --platform linux-x86_64 "${args[@]}"
- name: Verify downloaded hashes and profile
run: |
args=()
if [ "$GITHUB_EVENT_NAME" = pull_request ]; then args=(--unsigned); fi
nix develop --no-update-lock-file . -c ./scripts/verify-release.sh "$MAPLE_RELEASE_PROFILE" "../../artifacts" "${args[@]}"
- name: Smoke downloaded AppImage without Nix
nix develop --no-update-lock-file . -c ./scripts/verify-release.sh "$MAPLE_RELEASE_PROFILE" "../../artifacts" "${args[@]}" --platform linux-x86_64
- name: Smoke downloaded Linux bundle without Nix
working-directory: .
run: |
set -euo pipefail
image="$(find artifacts -maxdepth 1 -type f -name '*.AppImage')"
test -n "$image"
test "$(printf '%s\n' "$image" | wc -l)" -eq 1
shopt -s nullglob
archives=(artifacts/*-linux-x86_64.tar.gz)
test "${#archives[@]}" -eq 1
test -d /etc/ssl/certs
docker run --rm --platform linux/amd64 --user 65534:65534 \
--network none --read-only --tmpfs /tmp:exec,mode=1777 \
--volume "$PWD/artifacts:/artifacts:ro" \
--volume "$PWD/apps/maple-agent/scripts/smoke-linux-bundle-host.sh:/checks/smoke-linux-bundle-host.sh:ro" \
--volume /etc/ssl/certs:/etc/ssl/certs:ro \
ubuntu@sha256:f610ab94648195aa356059f5b41d6085c9d4d903c072430cdd1af7bdb646106b /bin/sh -ec '
cd /tmp
/artifacts/*.AppImage --appimage-extract >/dev/null
./squashfs-root/AppRun --version >&2
./squashfs-root/AppRun --build-info
tar -xzpf /artifacts/*-linux-x86_64.tar.gz
./maple-agent*.app/AppRun --version >&2
./maple-agent*.app/AppRun --build-info
MAPLE_AGENT_SMOKE_DIAGNOSTICS=1 /bin/sh /checks/smoke-linux-bundle-host.sh ./maple-agent*.app/AppRun >&2
' > "$RUNNER_TEMP/portable-build-info.json"
python3 - "$MAPLE_RELEASE_PROFILE" "$RUNNER_TEMP/portable-build-info.json" artifacts/build-info.json <<'PY'
import json
import sys
actual = json.load(open(sys.argv[2]))
expected = json.load(open(sys.argv[3]))
if actual != expected or actual.get("profile") != sys.argv[1]:
raise SystemExit("portable AppImage build profile differs from verified package")
raise SystemExit("portable Linux bundle build profile differs from verified package")
print("Verified Agent launch and embedded profile without a Nix installation")
PY
4 changes: 2 additions & 2 deletions .github/workflows/agent-updates-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ on:
required: true
type: string
include_linux:
description: Include Linux only after its packaged update launch gate passes
description: Publish this release for Linux too (disable only for a legacy macOS-only run)
required: true
default: false
default: true
type: boolean

permissions:
Expand Down
16 changes: 8 additions & 8 deletions apps/maple-agent/Cargo.lock

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

2 changes: 1 addition & 1 deletion apps/maple-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ members = ["crates/maple-agent", "crates/maple-billing", "crates/maple-agent-upd

[workspace.package]
edition = "2024"
version = "0.0.3"
version = "0.0.4"
license = "MIT"

[workspace.dependencies]
Expand Down
16 changes: 9 additions & 7 deletions apps/maple-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ Goose, with an independent dependency graph.
- Billing status from the Maple billing API.
- Desktop notifications when a task finishes or asks a question while the
window is not focused.
- Release check on launch: a banner links to a newer GitHub release.
- Signed updates for packaged Prod: background download and Restart to Update;
read-only installations offer a verified manual download.
Nothing is downloaded or installed by the app.
- Window size and maximized state persist between launches.

Expand Down Expand Up @@ -305,7 +306,7 @@ just release # local release build only
For hosted Dev and Prod packages, see [desktop builds](docs/desktop-builds.md)
and [build profiles](docs/release-profiles.md). The desktop build workflow
produces both profiles on protected `master`; macOS packages use Developer ID
signing and notarization, and Linux packages are portable AppImages.
signing and notarization, and Linux packages are portable user-local tar bundles.

The root also offers `just agent-check`, `just agent-build`, and
`just agent-dev`. Native development on macOS requires full Xcode at
Expand Down Expand Up @@ -589,17 +590,18 @@ check.
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
variables cannot replace them. Packaged Prod builds check the compiled feed, while
publication remains disabled pending owner service setup. Invitations still
require the signed installed-update rehearsal.
variables cannot replace them. Packaged Prod builds check the compiled platform
feed; publication is a separate protected operation. Invitations require the
signed installed-update rehearsal for each supported platform.
An empty-key profile still reports that updates are not configured and makes no
update requests.

Updates authenticate product, channel, version, target, byte size and SHA-256
before staging. Automatic downloads default on; installation requires the
explicit Restart to Update action, fresh feed revalidation and profile-host
shutdown. Read-only installations offer a manual download. Linux automatic
replacement stays disabled until its packaged update gate is independently
shutdown. Read-only installations offer a manual download. Linux x86_64 uses
a user-local directory bundle, with atomic directory exchange and relaunch
through its AppRun entrypoint. Its packaged update gate must be independently
validated. See [updater development and activation](docs/updates.md).

The [desktop build workflow](docs/desktop-builds.md) provides Actions artifacts
Expand Down
4 changes: 3 additions & 1 deletion apps/maple-agent/app/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ fn main() {
let pcr = std::env::var("VITE_OPEN_SECRET_PCR_ENVIRONMENT").ok();
let (channel, profile) = release_profile::select_profile(channel.as_deref(), pcr.as_deref())
.expect("validated Maple Agent build profile");
let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("Cargo target OS");
let update_feed_url = profile.update_feed_for_target_os(&target_os);
let metadata = serde_json::json!({
"profile": channel,
"display_name": profile.display_name,
Expand All @@ -68,7 +70,7 @@ fn main() {
"git_revision": revision,
"source_sha": source_sha,
"update_tag_prefix": profile.update_tag_prefix,
"update_feed_url": profile.update_feed_url,
"update_feed_url": update_feed_url,
"update_public_keys": profile.update_public_keys,
"prerelease": profile.prerelease,
});
Expand Down
20 changes: 18 additions & 2 deletions apps/maple-agent/app/src/desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2704,8 +2704,7 @@ impl StartupError {
cx.background_executor()
.spawn(async move {
crate::host::stop(&crate::configured_api_url())?;
let exe = std::env::current_exe().map_err(|e| e.to_string())?;
std::process::Command::new(exe)
crate::self_launch::command()?
.spawn()
.map_err(|e| e.to_string())?;
Ok::<(), String>(())
Expand Down Expand Up @@ -2780,8 +2779,14 @@ impl Render for StartupError {
}

fn show_startup_error(error: String) {
#[cfg(target_os = "linux")]
let restart_path = crate::self_launch::executable().ok();
gpui_platform::application().run(move |cx| {
cx.set_app_identity(crate::profile::BUNDLE_ID, crate::profile::DISPLAY_NAME);
#[cfg(target_os = "linux")]
if let Some(path) = restart_path {
cx.set_restart_path(path);
}
cx.on_action(|_: &QuitApp, cx| quit_app(cx));
ui::menus::install(cx);
let bounds = Bounds::centered(None, size(px(560.), px(260.)), cx);
Expand Down Expand Up @@ -2836,6 +2841,15 @@ fn show_update_about(cx: &mut App) {
pub fn run() {
crate::init_logging(crate::LogOutput::FileAndStderr);
log::debug!("startup: logging ready at {} ms", crate::startup_elapsed());
#[cfg(target_os = "linux")]
let restart_path = match crate::self_launch::executable() {
Ok(path) => path,
Err(error) => {
log::error!("Could not start Maple: {error}");
show_startup_error(error);
return;
}
};

// The frontend starts with defaults until the host's bounded settings
// snapshot arrives. Locked startup never reads saved application data.
Expand All @@ -2857,6 +2871,8 @@ pub fn run() {
.with_assets(crate::assets::Assets)
.run(move |cx: &mut App| {
cx.set_app_identity(crate::profile::BUNDLE_ID, crate::profile::DISPLAY_NAME);
#[cfg(target_os = "linux")]
cx.set_restart_path(restart_path);
cx.set_quit_mode(gpui::QuitMode::Explicit);
log::debug!("startup: gpui app ready at {} ms", crate::startup_elapsed());
if let Err(error) = cx.text_system().add_fonts(
Expand Down
3 changes: 1 addition & 2 deletions apps/maple-agent/app/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,7 @@ async fn connect(
}

fn spawn_host() -> Result<(), String> {
let mut command =
std::process::Command::new(std::env::current_exe().map_err(|e| e.to_string())?);
let mut command = crate::self_launch::command()?;
command
.args(["host", "serve"])
.stdin(std::process::Stdio::null())
Expand Down
Loading
Loading