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
5 changes: 3 additions & 2 deletions apps/maple-agent/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions apps/maple-agent/app/assets/licenses/development.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Maple Agent third-party notices</title>
<title>Maple Work third-party notices</title>
<style>body{max-width:48rem;margin:2rem auto;padding:0 1rem;font:16px/1.5 system-ui}</style>
<h1>Third-party notices</h1>
<p>Maple Agent uses open-source software and data. Distributed packages include
<p>Maple Work uses open-source software and data. Distributed packages include
the license inventory for their platform alongside the application.</p>
<ul>
<li><a href="https://github.com/MaplePrivacyLabs/Maple/tree/master/apps/maple-agent">Maple Agent source and dependency manifests</a></li>
<li><a href="https://github.com/MaplePrivacyLabs/Maple/tree/master/apps/maple-agent">Maple Work source and dependency manifests</a></li>
<li><a href="https://github.com/MaplePrivacyLabs/Maple/blob/master/apps/maple-agent/app/assets/dict/LICENSE">SCOWL spelling dictionary acknowledgements</a></li>
<li>Passphrase generation uses the unmodified EFF large wordlist, Electronic Frontier Foundation, 2016:
<a href="https://www.eff.org/dice">wordlist</a>,
Expand Down
2 changes: 1 addition & 1 deletion apps/maple-agent/app/macos/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Maple Agent Debug</string>
<string>Maple Work Debug</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
Expand Down
6 changes: 3 additions & 3 deletions apps/maple-agent/app/src/hosted_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub(crate) const ATTEMPT_TIMEOUT: Duration = Duration::from_secs(15 * 60);
const READ_TIMEOUT: Duration = Duration::from_secs(2);
const MAX_REQUEST_BYTES: usize = 8192;
const CALLBACK_PATH: &str = "/auth/callback";
const RETURN_ERROR: &str = "Browser sign-in could not finish. Start again in Maple Agent.";
const RETURN_ERROR: &str = "Browser sign-in could not finish. Start again in Maple Work.";

pub(crate) struct HandoffListener {
listener: TcpListener,
Expand Down Expand Up @@ -215,12 +215,12 @@ fn completion_response(outcome: Result<bool, ()>) -> String {
let (title, message) = match outcome {
Ok(true) => ("Signed in", "You can close this tab."),
Ok(false) => (
"Signed in to Maple Agent",
"Signed in to Maple Work",
"Your session could not be saved. You will need to sign in again when you restart the app. You can close this tab.",
),
Err(()) => (
"Sign-in could not finish",
"Return to Maple Agent and start again. You can close this tab.",
"Return to Maple Work and start again. You can close this tab.",
),
};
let body = include_str!("hosted_oauth_complete.html")
Expand Down
2 changes: 1 addition & 1 deletion apps/maple-agent/app/src/hosted_oauth_complete.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maple Agent sign-in</title>
<title>Maple Work sign-in</title>
<script>history.replaceState(null, "", "/auth/callback");</script>
<style>
* { box-sizing: border-box; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ fn validate_destination(installed: &Path) -> Result<()> {
|| installed.starts_with("/Volumes")
{
return Err(InstallError::NotifyOnly(
"move Maple Agent to a writable installed application directory",
"move Maple Work to a writable installed application directory",
));
}
if fs::symlink_metadata(installed)?.file_type().is_symlink()
Expand Down
6 changes: 3 additions & 3 deletions apps/maple-agent/crates/maple-agent/src/acp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ impl AcpConnectionContext {
&& !available_models.iter().any(|available| available == model)
{
return Err(agent_client_protocol::Error::invalid_request().data(format!(
"This Maple Agent task uses model '{model}', which is no longer available; the task remains available in Maple Desktop"
"This Maple Work task uses model '{model}', which is no longer available; the task remains available in Maple Desktop"
)));
}
let session_servers = prepare_session_mcp(&request.mcp_servers)?;
Expand Down Expand Up @@ -1552,7 +1552,7 @@ impl AcpConnectionContext {
return Err(agent_client_protocol::Error::resource_not_found(Some(
session_id.to_string(),
))
.data("The Maple Agent task was removed outside this ACP connection"));
.data("The Maple Work task was removed outside this ACP connection"));
}
Err(error) => return Err(internal_acp_error(error)),
};
Expand Down Expand Up @@ -1762,7 +1762,7 @@ impl AcpConnectionContext {
.await
}
None => Err(agent_client_protocol::Error::internal_error()
.data("Maple Agent run ended without a terminal result")),
.data("Maple Work run ended without a terminal result")),
};
}
};
Expand Down
2 changes: 1 addition & 1 deletion apps/maple-agent/crates/maple-agent/src/acp/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ pub(super) fn find_acp_session<'a>(
.iter()
.find(|session| session.id == session_id)
.ok_or_else(|| {
"The requested Maple Agent task does not exist in the supplied project directory"
"The requested Maple Work task does not exist in the supplied project directory"
.to_string()
})
}
2 changes: 1 addition & 1 deletion apps/maple-agent/crates/maple-agent/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const DEFAULT_MCP_TIMEOUT_SECONDS: u64 = 300;
const AGENT_SERVICE_OPEN: u8 = 0;
const AGENT_SERVICE_DRAINING: u8 = 1;
const AGENT_SERVICE_DRAINING_ERROR: &str =
"Maple Agent services are draining and cannot accept new work";
"Maple Work services are draining and cannot accept new work";
const RUNTIME_NOT_RUNNING_ERROR: &str = "Agent runtime is not running";
const RUNTIME_SAVE_PENDING_ERROR: &str =
"Agent history is waiting to be saved. Retry saving before starting more work.";
Expand Down
4 changes: 2 additions & 2 deletions apps/maple-agent/crates/maple-agent/src/agent/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async fn cached_model_ids(
models
}
Err(error) => {
log::warn!("Failed to fetch the Maple Agent model catalog: {error}");
log::warn!("Failed to fetch the Maple Work model catalog: {error}");
Vec::new()
}
}
Expand Down Expand Up @@ -158,7 +158,7 @@ impl AgentRuntimeHandle {
match self.api().await?.model_catalog().await {
Ok(catalog) => Ok(Some(catalog_entry(&catalog, model_id).unwrap_or_default())),
Err(error) => {
log::warn!("Failed to fetch the Maple Agent model catalog: {error}");
log::warn!("Failed to fetch the Maple Work model catalog: {error}");
Ok(None)
}
}
Expand Down
4 changes: 2 additions & 2 deletions apps/maple-agent/docs/desktop-builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ previews.

| Profile | Application | macOS bundle ID | Configuration/data namespace | Update delivery |
| --- | --- | --- | --- | --- |
| Dev | Maple Agent Dev | `cloud.opensecret.maple.agent.dev` | `maple-agent-dev` | Disabled; master Actions artifacts |
| Prod | Maple Agent | `cloud.opensecret.maple.agent` | `maple-agent-prod` | Signed Agent feed with primary/backup pins; publication separately gated |
| 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 |

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
15 changes: 11 additions & 4 deletions apps/maple-agent/docs/release-profiles.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Maple Agent build profiles
# Maple Work build profiles

`MAPLE_RELEASE_PROFILE=dev` or `prod` selects a packaged profile at compile
time. `release-profiles.json` is the shared public configuration used by both
Expand All @@ -8,8 +8,15 @@ ID, or PCR trust environment.

| Profile | Application | Bundle ID | State namespace | API / billing trust |
| --- | --- | --- | --- | --- |
| Dev | Maple Agent Dev | `cloud.opensecret.maple.agent.dev` | `maple-agent-dev` | Hosted Dev API and billing; development PCR |
| Prod | Maple Agent | `cloud.opensecret.maple.agent` | `maple-agent-prod` | Hosted Prod API and billing; production PCR |
| Dev | Maple Work Dev | `cloud.opensecret.maple.agent.dev` | `maple-agent-dev` | Hosted Dev API and billing; development PCR |
| Prod | Maple Work | `cloud.opensecret.maple.agent` | `maple-agent-prod` | Hosted Prod API and billing; production PCR |

Display branding is independent of installation identity. macOS keeps the
`Maple Agent.app` and `Maple Agent Dev.app` filenames and updater archive roots;
Linux keeps `maple-agent.app` and `maple-agent-dev.app`. Executables, bundle IDs,
state namespaces, settings, update feeds, and artifact filenames are unchanged.
macOS uses `Resources/en.lproj/InfoPlist.strings` for the new display name while
the base `CFBundleDisplayName` matches the existing bundle filename for Finder.

The API endpoints and client ID match `scripts/ci/_common.sh`'s existing
Research Dev and Prod service selection. Billing browser pricing, checkout
Expand Down Expand Up @@ -43,7 +50,7 @@ state. Installing both channels therefore keeps separate sign-ins and history.
With `MAPLE_RELEASE_PROFILE` unset, the binary reports `unpackaged`, retains the
existing `maple-agent` namespace, runtime endpoint/client overrides, explicit
compile-time PCR selection, and legacy `maple-gpui` adoption. Its debug bundle
identity is `cloud.opensecret.maple.agent.debug` and display name is Maple Agent
identity is `cloud.opensecret.maple.agent.debug` and display name is Maple Work
Debug. Release packaging accepts only Dev or Prod metadata.

`maple-agent --build-info` prints one JSON object and exits before changing the
Expand Down
4 changes: 2 additions & 2 deletions apps/maple-agent/release-profiles.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"dev": {
"display_name": "Maple Agent Dev",
"display_name": "Maple Work Dev",
"bundle_id": "cloud.opensecret.maple.agent.dev",
"data_namespace": "maple-agent-dev",
"api_url": "https://enclave.secretgpt.ai",
Expand All @@ -16,7 +16,7 @@
"prerelease": true
},
"prod": {
"display_name": "Maple Agent",
"display_name": "Maple Work",
"bundle_id": "cloud.opensecret.maple.agent",
"data_namespace": "maple-agent-prod",
"api_url": "https://enclave.trymaple.ai",
Expand Down
2 changes: 1 addition & 1 deletion apps/maple-agent/release_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ pub fn select_profile(
}]
.clone();
if channel == "unpackaged" {
profile.display_name = "Maple Agent Debug".into();
profile.display_name = "Maple Work Debug".into();
profile.bundle_id = "cloud.opensecret.maple.agent.debug".into();
profile.data_namespace = "maple-agent".into();
// Hosted return is admitted only by packaged profiles. Local
Expand Down
4 changes: 2 additions & 2 deletions apps/maple-agent/scripts/generate-third-party-notices.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
def render(inventory, component, target, default_features, features):
feature_description = ", ".join(features) or "none"
blocks = [
"Maple Agent — Third-party notices\n",
"Maple Work — Third-party notices\n",
"This distribution includes software and data under the following licenses.\n"
f"Application: maple-agent-app; target: {target}.\n"
f"Default features: {default_features}; additional features: {feature_description}.\n"
Expand All @@ -42,7 +42,7 @@ def render(inventory, component, target, default_features, features):
text = ("\n" + "=" * 78 + "\n\n").join(blocks).rstrip() + "\n"
document = ('<!doctype html><html lang="en"><meta charset="utf-8">'
'<meta name="viewport" content="width=device-width">'
'<title>Maple Agent third-party notices</title>'
'<title>Maple Work third-party notices</title>'
'<style>body{max-width:76rem;margin:2rem auto;padding:0 1rem;'
'font:15px/1.5 system-ui}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style>'
'<h1>Third-party notices</h1><pre>' + html.escape(text) + '</pre></html>\n')
Expand Down
4 changes: 2 additions & 2 deletions apps/maple-agent/scripts/linux-install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions apps/maple-agent/scripts/macos-debug-plist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}))
8 changes: 5 additions & 3 deletions apps/maple-agent/scripts/macos-release-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
}

Expand Down
23 changes: 18 additions & 5 deletions apps/maple-agent/scripts/release-info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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):
Expand Down Expand Up @@ -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")
Expand All @@ -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])
Expand Down
3 changes: 3 additions & 0 deletions apps/maple-agent/scripts/test_linux_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion apps/maple-agent/scripts/test_linux_release_appimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading