From fd07861bc3d2d55fcfd772a7711dc62386a31a9e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 24 Feb 2026 14:20:37 +0000 Subject: [PATCH] fix(ios): harden team-id profile fallback and tests --- CHANGELOG.md | 1 + scripts/ios-team-id.sh | 5 +- test/scripts/ios-team-id.test.ts | 195 +++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 test/scripts/ios-team-id.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ce97f60c7f9..7319d8f7389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Exec approvals: treat bare allowlist `*` as a true wildcard for parsed executables, including unresolved PATH lookups, so global opt-in allowlists work as configured. (#25250) Thanks @widingmarcus-cyber. - Gateway/Auth: allow trusted-proxy authenticated Control UI websocket sessions to skip device pairing when device identity is absent, preventing false `pairing required` failures behind trusted reverse proxies. (#25428) Thanks @SidQin-cyber. - Agents/Tool dispatch: await block-reply flush before tool execution starts so buffered block replies preserve message ordering around tool calls. (#25427) Thanks @SidQin-cyber. +- iOS/Signing: improve `scripts/ios-team-id.sh` for Xcode 16+ by falling back to Xcode-managed provisioning profiles, add actionable guidance when an Apple account exists but no Team ID can be resolved, and ignore Xcode `xcodebuild` output directories (`apps/ios/build`, `apps/shared/OpenClawKit/build`, `Swabble/build`). (#22773) Thanks @brianleach. - macOS/Menu bar: stop reusing the injector delegate for the "Usage cost (30 days)" submenu to prevent recursive submenu injection loops when opening cost history. (#25341) Thanks @yingchunbai. - Control UI/Chat images: centralize safe external URL opening for image clicks (allowlist `http/https/blob` + opt-in `data:image/*`) and enforce opener isolation (`noopener,noreferrer` + `window.opener = null`) to prevent tabnabbing/unsafe schemes. (#25444) Thanks @shakkernerd. - CLI/Doctor: correct stale recovery hints to use valid commands (`openclaw gateway status --deep` and `openclaw configure --section model`). (#24485) Thanks @chilu18. diff --git a/scripts/ios-team-id.sh b/scripts/ios-team-id.sh index 8c27c03d9b4..4357722190d 100755 --- a/scripts/ios-team-id.sh +++ b/scripts/ios-team-id.sh @@ -94,7 +94,10 @@ load_teams_from_xcode_managed_profiles() { | /usr/bin/python3 -c ' import plistlib, sys try: - d = plistlib.load(sys.stdin.buffer) + raw = sys.stdin.buffer.read() + if not raw: + raise SystemExit(0) + d = plistlib.loads(raw) for tid in d.get("TeamIdentifier", []): print(tid) except Exception: diff --git a/test/scripts/ios-team-id.test.ts b/test/scripts/ios-team-id.test.ts new file mode 100644 index 00000000000..242e897945d --- /dev/null +++ b/test/scripts/ios-team-id.test.ts @@ -0,0 +1,195 @@ +import { execFileSync } from "node:child_process"; +import { chmodSync } from "node:fs"; +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh"); + +async function writeExecutable(filePath: string, body: string): Promise { + await writeFile(filePath, body, "utf8"); + chmodSync(filePath, 0o755); +} + +function runScript( + homeDir: string, + extraEnv: Record = {}, +): { + ok: boolean; + stdout: string; + stderr: string; +} { + const binDir = path.join(homeDir, "bin"); + const env = { + ...process.env, + HOME: homeDir, + PATH: `${binDir}:${process.env.PATH ?? ""}`, + ...extraEnv, + }; + try { + const stdout = execFileSync("bash", [SCRIPT], { + env, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + return { ok: true, stdout: stdout.trim(), stderr: "" }; + } catch (error) { + const e = error as { + stdout?: string | Buffer; + stderr?: string | Buffer; + }; + const stdout = typeof e.stdout === "string" ? e.stdout : (e.stdout?.toString("utf8") ?? ""); + const stderr = typeof e.stderr === "string" ? e.stderr : (e.stderr?.toString("utf8") ?? ""); + return { ok: false, stdout: stdout.trim(), stderr: stderr.trim() }; + } +} + +describe("scripts/ios-team-id.sh", () => { + it("falls back to Xcode-managed provisioning profiles when preference teams are empty", async () => { + const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-")); + const binDir = path.join(homeDir, "bin"); + await mkdir(binDir, { recursive: true }); + await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true }); + await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), { + recursive: true, + }); + await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), ""); + await writeFile( + path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"), + "stub", + ); + + await writeExecutable( + path.join(binDir, "plutil"), + `#!/usr/bin/env bash +echo '{}'`, + ); + await writeExecutable( + path.join(binDir, "defaults"), + `#!/usr/bin/env bash +if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then + echo '(identifier = "dev@example.com";)' + exit 0 +fi +exit 0`, + ); + await writeExecutable( + path.join(binDir, "security"), + `#!/usr/bin/env bash +if [[ "$1" == "cms" && "$2" == "-D" ]]; then + cat <<'PLIST' + + + + + TeamIdentifier + + ABCDE12345 + + + +PLIST + exit 0 +fi +exit 0`, + ); + + const result = runScript(homeDir); + expect(result.ok).toBe(true); + expect(result.stdout).toBe("ABCDE12345"); + }); + + it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => { + const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-")); + const binDir = path.join(homeDir, "bin"); + await mkdir(binDir, { recursive: true }); + await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true }); + await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), ""); + + await writeExecutable( + path.join(binDir, "plutil"), + `#!/usr/bin/env bash +echo '{}'`, + ); + await writeExecutable( + path.join(binDir, "defaults"), + `#!/usr/bin/env bash +if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then + echo '(identifier = "dev@example.com";)' + exit 0 +fi +echo "Domain/default pair of (com.apple.dt.Xcode, $3) does not exist" >&2 +exit 1`, + ); + await writeExecutable( + path.join(binDir, "security"), + `#!/usr/bin/env bash +exit 1`, + ); + + const result = runScript(homeDir); + expect(result.ok).toBe(false); + expect(result.stderr).toContain("An Apple account is signed in to Xcode"); + expect(result.stderr).toContain("IOS_DEVELOPMENT_TEAM"); + }); + + it("honors IOS_PREFERRED_TEAM_ID when multiple profile teams are available", async () => { + const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-")); + const binDir = path.join(homeDir, "bin"); + await mkdir(binDir, { recursive: true }); + await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true }); + await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), { + recursive: true, + }); + await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), ""); + await writeFile( + path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"), + "stub1", + ); + await writeFile( + path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "two.mobileprovision"), + "stub2", + ); + + await writeExecutable( + path.join(binDir, "plutil"), + `#!/usr/bin/env bash +echo '{}'`, + ); + await writeExecutable( + path.join(binDir, "defaults"), + `#!/usr/bin/env bash +if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then + echo '(identifier = "dev@example.com";)' + exit 0 +fi +exit 0`, + ); + await writeExecutable( + path.join(binDir, "security"), + `#!/usr/bin/env bash +if [[ "$1" == "cms" && "$2" == "-D" ]]; then + if [[ "$4" == *"one.mobileprovision" ]]; then + cat <<'PLIST' + + +TeamIdentifierAAAAA11111 +PLIST + exit 0 + fi + cat <<'PLIST' + + +TeamIdentifierBBBBB22222 +PLIST + exit 0 +fi +exit 0`, + ); + + const result = runScript(homeDir, { IOS_PREFERRED_TEAM_ID: "BBBBB22222" }); + expect(result.ok).toBe(true); + expect(result.stdout).toBe("BBBBB22222"); + }); +});