diff --git a/apps/maple-agent/README.md b/apps/maple-agent/README.md index a1eb0bfa7..9f01c1a45 100644 --- a/apps/maple-agent/README.md +++ b/apps/maple-agent/README.md @@ -1,4 +1,4 @@ -# Maple Agent (GPUI prototype) +# Maple Work (GPUI prototype) Maple’s desktop-v2 prototype lives in `apps/maple-agent/` in this monorepo. Its Cargo package is `maple-agent-app` and its executable is `maple-agent`. It rebuilds Maple Agent Mode in @@ -331,7 +331,8 @@ identity, discovers and embeds any Swift compatibility libraries required by native dependencies, signs nested code before sealing the bundle, and prints the exact bundle path to launch. This requires a full Xcode toolchain but does not require a Developer ID or produce a release artifact. The default ad hoc -bundle is named Maple Agent Debug with identifier +bundle displays Maple Work Debug and keeps the filename `Maple Agent Debug.app` +with identifier `cloud.opensecret.maple.agent.debug`, separate from both packaged profiles. Its identity changes when Maple is rebuilt, so macOS may require the development app's privacy grants again. Set `MAPLE_DEBUG_CODESIGN_IDENTITY` to the name or diff --git a/apps/maple-agent/app/assets/licenses/development.html b/apps/maple-agent/app/assets/licenses/development.html index 6787e34a7..8835b3b87 100644 --- a/apps/maple-agent/app/assets/licenses/development.html +++ b/apps/maple-agent/app/assets/licenses/development.html @@ -2,13 +2,13 @@ -
Maple Agent uses open-source software and data. Distributed packages include +
Maple Work uses open-source software and data. Distributed packages include the license inventory for their platform alongside the application.
' + html.escape(text) + '\n') diff --git a/apps/maple-agent/scripts/linux-install.py b/apps/maple-agent/scripts/linux-install.py index 4e53ec941..db8c221d8 100644 --- a/apps/maple-agent/scripts/linux-install.py +++ b/apps/maple-agent/scripts/linux-install.py @@ -21,8 +21,8 @@ FIELDS = {"format", "product", "profile", "version", "build", "source_sha", "bundle_id", "entrypoint", "executable"} PROFILES = { - "prod": ("maple-agent.app", "maple-agent", "cloud.opensecret.maple.agent", "Maple Agent"), - "dev": ("maple-agent-dev.app", "maple-agent-dev", "cloud.opensecret.maple.agent.dev", "Maple Agent Dev"), + "prod": ("maple-agent.app", "maple-agent", "cloud.opensecret.maple.agent", "Maple Work"), + "dev": ("maple-agent-dev.app", "maple-agent-dev", "cloud.opensecret.maple.agent.dev", "Maple Work Dev"), } MAX_ENTRIES = 20_000 MAX_FILE = 2 * 1024**3 diff --git a/apps/maple-agent/scripts/macos-debug-plist.py b/apps/maple-agent/scripts/macos-debug-plist.py index 153bcdfd4..c4b938c9c 100644 --- a/apps/maple-agent/scripts/macos-debug-plist.py +++ b/apps/maple-agent/scripts/macos-debug-plist.py @@ -43,3 +43,10 @@ def configure(plist: dict, environment: dict[str, str]) -> dict: configured = configure(plistlib.load(source), os.environ) with path.open("wb") as output: plistlib.dump(configured, output) + # Localize presentation while retaining the debug bundle's installed name. + localized = path.parent / "Resources/en.lproj/InfoPlist.strings" + localized.parent.mkdir(parents=True, exist_ok=True) + localized.write_bytes(plistlib.dumps({ + "CFBundleDisplayName": configured["CFBundleName"], + "CFBundleName": configured["CFBundleName"], + })) diff --git a/apps/maple-agent/scripts/macos-release-app.sh b/apps/maple-agent/scripts/macos-release-app.sh index d5d4ec694..3345f9bd5 100755 --- a/apps/maple-agent/scripts/macos-release-app.sh +++ b/apps/maple-agent/scripts/macos-release-app.sh @@ -70,6 +70,8 @@ macos_release_main() { [[ -x "$binary" && -f "$output_dir/build-info.json" ]] || { echo "missing release binary/build-info" >&2; return 1; } local app_name="${MAPLE_PACKAGE_APP_NAME:?}" bundle_id="${MAPLE_PACKAGE_BUNDLE_ID:?}" local profile="${MAPLE_PACKAGE_CHANNEL:?}" build_number="${MAPLE_PACKAGE_BUILD_NUMBER:-1}" + local bundle_name + bundle_name="$(python3 "$component/scripts/release-info.py" bundle-name "$profile" "$output_dir/build-info.json")" local arches arches="$(macos_native lipo -archs "$binary")" [[ "$arches" == arm64 ]] || { echo "release binary must contain exactly the arm64 slice" >&2; return 1; } @@ -83,7 +85,7 @@ macos_release_main() { trap 'exit 130' INT trap 'exit 143' TERM trap 'exit 129' HUP - local app="$macos_release_staging/$app_name.app" + local app="$macos_release_staging/$bundle_name.app" local frameworks="$app/Contents/Frameworks" executable="$app/Contents/MacOS/maple-agent" mkdir -p "$frameworks" "$app/Contents/MacOS" "$app/Contents/Resources" cp "$binary" "$executable" @@ -222,7 +224,7 @@ print(matches[0])' "$APPLE_TEAM_ID")" local image_root="$macos_release_staging/image" mkdir "$image_root" chmod 0755 "$image_root" - macos_native ditto "$app" "$image_root/$app_name.app" + macos_native ditto "$app" "$image_root/$bundle_name.app" ln -s /Applications "$image_root/Applications" # hdiutil's own -srcfolder size estimate can come up short and fail with # "No space left on device". Size the image from the payload with headroom, @@ -256,7 +258,7 @@ print(-(-total//(1024*1024)))' "$image_root")" macos_native xcrun stapler validate "$output_dir/$basename.dmg" macos_native spctl --assess --type open --context context:primary-signature --verbose=2 "$output_dir/$basename.dmg" fi - macos_archive_app "$macos_release_staging" "$app_name" "$output_dir/$basename.app.tar.gz" + macos_archive_app "$macos_release_staging" "$bundle_name" "$output_dir/$basename.app.tar.gz" chmod 0644 "$output_dir/$basename.dmg" "$output_dir/$basename.app.tar.gz" } diff --git a/apps/maple-agent/scripts/release-info.py b/apps/maple-agent/scripts/release-info.py index 536d9fbe8..69cad1cf4 100755 --- a/apps/maple-agent/scripts/release-info.py +++ b/apps/maple-agent/scripts/release-info.py @@ -94,7 +94,8 @@ def write_plist(info, path, build_number): raise ValueError("package build number must be a positive integer") plist = plistlib.loads((COMPONENT / "app/macos/Info.plist").read_bytes()) plist.update({ - "CFBundleDisplayName": info["display_name"], + # Finder localizes only when this base name matches the bundle path. + "CFBundleDisplayName": macos_bundle_name(info["profile"]), "CFBundleName": info["display_name"], "CFBundleIdentifier": info["bundle_id"], "CFBundleShortVersionString": info["version"].split("-", 1)[0], @@ -107,6 +108,17 @@ def write_plist(info, path, build_number): }) plist.pop("LSEnvironment", None) Path(path).write_bytes(plistlib.dumps(plist)) + localized = Path(path).parent / "Resources/en.lproj/InfoPlist.strings" + localized.parent.mkdir(parents=True, exist_ok=True) + localized.write_bytes(plistlib.dumps({ + "CFBundleDisplayName": info["display_name"], + "CFBundleName": info["display_name"], + })) + + +def macos_bundle_name(profile): + # Installed paths and updater archive roots predate the display branding. + return {"dev": "Maple Agent Dev", "prod": "Maple Agent"}[profile] def linux_package_name(info): @@ -285,7 +297,7 @@ def payload(directory): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("operation", choices=("validate", "field", "plist", "manifest", "verify", "extract-app", "compare-apps")) + parser.add_argument("operation", choices=("validate", "field", "bundle-name", "plist", "manifest", "verify", "extract-app", "compare-apps")) parser.add_argument("profile", choices=("dev", "prod")) parser.add_argument("path") parser.add_argument("--source-sha") @@ -299,14 +311,15 @@ def main(): print(json.dumps(verify_artifacts(args.profile, args.path, args.unsigned, args.source_sha, args.platform))) return if args.operation == "extract-app": - app_name = json.loads((COMPONENT / "release-profiles.json").read_text())[args.profile]["display_name"] - extract_app_archive(args.path, args.output, app_name) + extract_app_archive(args.path, args.output, macos_bundle_name(args.profile)) return if args.operation == "compare-apps": compare_app_payloads(args.path, args.output) return info = read_info(args.profile, args.path, args.source_sha, args.platform) - if args.operation == "field": + if args.operation == "bundle-name": + print(macos_bundle_name(args.profile)) + elif args.operation == "field": if args.field not in BUILD_FIELDS: raise ValueError("field is not public release metadata") print(info[args.field]) diff --git a/apps/maple-agent/scripts/test_linux_install.py b/apps/maple-agent/scripts/test_linux_install.py index b947d6afa..04c683959 100644 --- a/apps/maple-agent/scripts/test_linux_install.py +++ b/apps/maple-agent/scripts/test_linux_install.py @@ -98,6 +98,8 @@ def test_first_install_copies_bundle_and_integrates_without_profile_data_changes self.assertEqual(stat.S_IMODE(paths["wrapper"].stat().st_mode), 0o755) self.assertEqual(stat.S_IMODE(paths["receipt"].stat().st_mode), 0o600) desktop = paths["desktop"].read_text() + self.assertIn("Name=Maple Work\n", desktop) + self.assertEqual(paths["desktop"].name, "cloud.opensecret.maple.agent.desktop") self.assertIn("Exec=\"", desktop) self.assertIn("%%", desktop) self.assertIn(" %U\n", desktop) @@ -113,6 +115,7 @@ def test_dev_and_prod_have_distinct_bundle_commands_and_desktop_entries(self): self.assertEqual(dev["bundle"].name, "maple-agent-dev.app") self.assertEqual(dev["wrapper"].name, "maple-agent-dev") self.assertEqual(dev["desktop"].name, "cloud.opensecret.maple.agent.dev.desktop") + self.assertIn("Name=Maple Work Dev\n", dev["desktop"].read_text()) self.assertTrue(set(prod.values()).isdisjoint(dev.values())) def test_every_preexisting_managed_path_is_refused_without_replacement(self): diff --git a/apps/maple-agent/scripts/test_linux_release_appimage.py b/apps/maple-agent/scripts/test_linux_release_appimage.py index a6ee32a24..db12bf25f 100644 --- a/apps/maple-agent/scripts/test_linux_release_appimage.py +++ b/apps/maple-agent/scripts/test_linux_release_appimage.py @@ -25,7 +25,7 @@ HEADER = b"\x7fELF\x02\x01" + b"\x00" * 12 + b"\x3e\x00" METADATA = { "channel": "dev", - "app_name": "Maple Agent Dev", + "app_name": "Maple Work Dev", "bundle_id": "cloud.opensecret.maple.agent.dev", "version": "0.1.0", "build_number": "1", diff --git a/apps/maple-agent/scripts/test_release_packaging.py b/apps/maple-agent/scripts/test_release_packaging.py index 4b36f5e31..4839bf35d 100644 --- a/apps/maple-agent/scripts/test_release_packaging.py +++ b/apps/maple-agent/scripts/test_release_packaging.py @@ -289,7 +289,7 @@ def setUp(self): shutil.copy2(SCRIPTS.parent / "app/packaging/macos-entitlements.plist", entitlements) self.info = {**profile_info("dev"), "profile": "dev", "version": "0.1.0", "source_sha": "a" * 40, "git_revision": "a" * 8} - self.bundle = self.root / f"{self.info['display_name']}.app" + self.bundle = self.root / f"{release_info.macos_bundle_name('dev')}.app" binary = self.bundle / "Contents/MacOS/maple-agent" binary.parent.mkdir(parents=True) # Metadata-only Mach-O cannot execute. A successful static verification @@ -392,6 +392,33 @@ def test_profile_and_source_must_match_the_binary(self): with self.subTest(profile=profile, source=source), self.assertRaises(ValueError): release_info.read_info(profile, self.path, source) + def test_display_branding_preserves_installed_bundle_names_and_metadata_contract(self): + for profile, display, bundle in ( + ("prod", "Maple Work", "Maple Agent"), + ("dev", "Maple Work Dev", "Maple Agent Dev"), + ): + with self.subTest(profile=profile): + info = {**self.info, **profile_info(profile), "profile": profile} + self.path.write_text(json.dumps(info)) + result = subprocess.run( + [sys.executable, str(SCRIPTS / "release-info.py"), "bundle-name", profile, str(self.path)], + check=True, capture_output=True, text=True, + ) + self.assertEqual(result.stdout.strip(), bundle) + self.assertEqual(info["display_name"], display) + output = self.directory / "Info.plist" + release_info.write_plist(info, output, "42") + plist = plistlib.loads(output.read_bytes()) + self.assertEqual(plist["CFBundleDisplayName"], bundle) + self.assertEqual(plist["CFBundleName"], display) + localized = output.parent / "Resources/en.lproj/InfoPlist.strings" + self.assertEqual(plistlib.loads(localized.read_bytes()), { + "CFBundleDisplayName": display, "CFBundleName": display, + }) + self.assertEqual(plist["CFBundleIdentifier"], info["bundle_id"]) + self.assertEqual(plist["CFBundleExecutable"], "maple-agent") + self.assertEqual(set(info), release_info.BUILD_FIELDS) + def test_dirty_or_incomplete_source_revision_is_rejected(self): for revision in ("aaaaaaaa-dirty", "unknown", "bbbbbbbb"): self.info["git_revision"] = revision @@ -399,6 +426,24 @@ def test_dirty_or_incomplete_source_revision_is_rejected(self): with self.subTest(revision=revision), self.assertRaises(ValueError): release_info.read_info("dev", self.path) + def test_debug_bundle_localizes_branding_without_changing_its_identity(self): + output = self.directory / "Maple Agent Debug.app/Contents/Info.plist" + output.parent.mkdir(parents=True) + shutil.copyfile(SCRIPTS.parent / "app/macos/Info.plist", output) + subprocess.run( + [sys.executable, str(SCRIPTS / "macos-debug-plist.py"), str(output)], + env=dict(os.environ, MAPLE_DEBUG_BUNDLE_ID="cloud.opensecret.maple.agent.debug"), + check=True, capture_output=True, text=True, + ) + plist = plistlib.loads(output.read_bytes()) + self.assertEqual(plist["CFBundleIdentifier"], "cloud.opensecret.maple.agent.debug") + self.assertEqual(plist["CFBundleExecutable"], "maple-agent") + self.assertEqual(plist["CFBundleDisplayName"], "Maple Agent Debug") + localized = output.parent / "Resources/en.lproj/InfoPlist.strings" + self.assertEqual(plistlib.loads(localized.read_bytes()), { + "CFBundleDisplayName": "Maple Work Debug", "CFBundleName": "Maple Work Debug", + }) + def test_endpoint_or_state_namespace_drift_is_rejected(self): for field, value in (("api_url", "https://wrong.invalid"), ("data_namespace", "maple-agent-prod")): original = self.info[field] @@ -670,6 +715,17 @@ def test_extracts_exact_expected_app_and_compares_every_payload_file(self): with self.assertRaises(ValueError): release_info.compare_app_payloads(first, second) + def test_profile_extraction_keeps_the_installed_name_after_rebranding(self): + self.write_archive() + output = self.directory / "profile-output" + subprocess.run( + [sys.executable, str(SCRIPTS / "release-info.py"), "extract-app", "dev", + str(self.archive), "--output", str(output)], + check=True, capture_output=True, text=True, + ) + self.assertTrue((output / "Maple Agent Dev.app/Contents/maple-agent").is_file()) + self.assertFalse((output / "Maple Work Dev.app").exists()) + def test_rejects_traversal_unexpected_roots_and_link_escape(self): cases = [ ("/outside", "file", "fixture"), @@ -872,6 +928,9 @@ def test_inside_out_signing_notarization_and_ephemeral_keychain_cleanup(self): self.assert_secrets_not_returned(result) archive = next(call for call in calls if call[0] == "tar") self.assertEqual(archive[1:5], ["--no-xattrs", "--no-fflags", "--no-acls", "--no-mac-metadata"]) + self.assertEqual(archive[-1], "Maple Agent Dev.app") + image = next(call for call in calls if call[:2] == ["hdiutil", "create"]) + self.assertEqual(image[image.index("-volname") + 1], "Maple Work Dev") signs = [call for call in calls if call[0] == "codesign" and "--force" in call] self.assertTrue(signs[0][-1].endswith("libswiftFixture.dylib")) self.assertTrue(signs[1][-1].endswith(".app")) @@ -894,7 +953,12 @@ def test_inside_out_signing_notarization_and_ephemeral_keychain_cleanup(self): self.assert_search_list_restored(calls) self.assertFalse(list(self.directory.glob(".macos-release.*"))) with tarfile.open(self.directory / "fixture.app.tar.gz") as archive: - icon = archive.extractfile(f"{self.info['display_name']}.app/Contents/Resources/MapleAgent.icns").read() + bundle = release_info.macos_bundle_name("dev") + ".app" + plist = plistlib.loads(archive.extractfile(f"{bundle}/Contents/Info.plist").read()) + self.assertEqual(plist["CFBundleDisplayName"], "Maple Agent Dev") + localized = plistlib.loads(archive.extractfile(f"{bundle}/Contents/Resources/en.lproj/InfoPlist.strings").read()) + self.assertEqual(localized["CFBundleDisplayName"], "Maple Work Dev") + icon = archive.extractfile(f"{bundle}/Contents/Resources/MapleAgent.icns").read() self.assertEqual(icon, (SCRIPTS.parent / "app/packaging/maple-agent-dev.icns").read_bytes()) self.assertNotEqual(icon, (SCRIPTS.parent / "app/packaging/maple-agent.icns").read_bytes()) for member in archive: @@ -902,7 +966,7 @@ def test_inside_out_signing_notarization_and_ephemeral_keychain_cleanup(self): if member.isdir(): self.assertTrue(member.mode & 0o001, f"package directory is not publicly accessible: {member.name}") extracted = release_info.extract_app_archive( - self.directory / "fixture.app.tar.gz", self.directory / "extracted", self.info["display_name"] + self.directory / "fixture.app.tar.gz", self.directory / "extracted", release_info.macos_bundle_name("dev") ) self.assertTrue((extracted / "Contents/MacOS/maple-agent").is_file()) self.assertEqual(extracted.stat().st_mode & 0o777, 0o755) diff --git a/apps/maple-agent/scripts/verify-release.sh b/apps/maple-agent/scripts/verify-release.sh index 1483a0515..f21ea04d8 100755 --- a/apps/maple-agent/scripts/verify-release.sh +++ b/apps/maple-agent/scripts/verify-release.sh @@ -71,6 +71,7 @@ if [[ "$platform" == macos-aarch64 ]]; then mkdir "$mount" /usr/bin/hdiutil attach -readonly -nobrowse -noautoopen -mountpoint "$mount" "$directory/$package" >/dev/null app_name="$(python3 "$component/scripts/release-info.py" field "$profile" "$directory/build-info.json" --field display_name)" + bundle_name="$(python3 "$component/scripts/release-info.py" bundle-name "$profile" "$directory/build-info.json")" bundle_id="$(python3 "$component/scripts/release-info.py" field "$profile" "$directory/build-info.json" --field bundle_id)" verify_macos_app() { local app="$1" signature executable @@ -98,14 +99,19 @@ actual=plistlib.loads(open(sys.argv[1], "rb").read()) expected=plistlib.loads(open(sys.argv[2], "rb").read()) assert actual == expected, "downloaded app entitlements differ from the reviewed policy" PY - python3 - "$app/Contents/Info.plist" "$bundle_id" "$app_name" "$directory/build-info.json" <<'PY' + python3 - "$app/Contents/Info.plist" "$bundle_id" "$app_name" "$directory/build-info.json" "$bundle_name" <<'PY' import json,plistlib,re,sys +from pathlib import Path with open(sys.argv[1], "rb") as source: plist=plistlib.load(source) info=json.load(open(sys.argv[4])) assert plist["CFBundleIdentifier"] == sys.argv[2] -assert plist["CFBundleDisplayName"] == sys.argv[3] +assert plist["CFBundleDisplayName"] == sys.argv[5] assert plist["CFBundleName"] == sys.argv[3] +localized=Path(sys.argv[1]).parent / "Resources/en.lproj/InfoPlist.strings" +assert plistlib.loads(localized.read_bytes()) == { + "CFBundleDisplayName": sys.argv[3], "CFBundleName": sys.argv[3], +} assert plist["CFBundleExecutable"] == "maple-agent" assert plist["CFBundleShortVersionString"] == info["version"].split("-",1)[0] assert re.fullmatch(r"[1-9][0-9]*", plist["CFBundleVersion"]) @@ -127,12 +133,12 @@ PY fi fi } - dmg_app="$mount/$app_name.app" + dmg_app="$mount/$bundle_name.app" verify_macos_app "$dmg_app" archives=("$directory"/*.app.tar.gz) [[ ${#archives[@]} == 1 && -f "${archives[0]}" ]] || { echo "expected exactly one app archive" >&2; exit 1; } python3 "$component/scripts/release-info.py" extract-app "$profile" "${archives[0]}" --output "$temporary/archive" - archive_app="$temporary/archive/$app_name.app" + archive_app="$temporary/archive/$bundle_name.app" verify_macos_app "$archive_app" python3 "$component/scripts/release-info.py" compare-apps "$profile" "$dmg_app" --output "$archive_app" if [[ "$static" == false ]]; then