feat: wire ios push sandbox tooling

This commit is contained in:
joshavant
2026-06-22 19:11:00 -05:00
committed by Josh Avant
parent e08ef9f893
commit 760f86453e
11 changed files with 219 additions and 13 deletions

View File

@@ -12,7 +12,7 @@
"platform": "IOS",
"profileKey": "OPENCLAW_APP_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app",
"capabilities": ["PUSH_NOTIFICATIONS", "APP_GROUPS"],
"capabilities": ["PUSH_NOTIFICATIONS", "APP_GROUPS", "APP_ATTEST"],
"appGroups": ["group.ai.openclawfoundation.app.shared"]
},
{

View File

@@ -6,6 +6,7 @@ OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM)
OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClaw.entitlements
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp

View File

@@ -6,6 +6,7 @@
OPENCLAW_CODE_SIGN_STYLE = Manual
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClaw.entitlements
OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app

View File

@@ -495,6 +495,9 @@ def produce_services_for_target(target)
if target.fetch("capabilities").include?("APP_GROUPS")
services[:app_group] = "on"
end
if target.fetch("capabilities").include?("APP_ATTEST")
services[:app_attest] = "on"
end
services
end
@@ -605,6 +608,15 @@ def validate_match_profile_capabilities!(target)
)
end
end
if capabilities.include?("APP_ATTEST")
app_attest_environments = profile_plist_array_values(profile_path, "Entitlements:com.apple.developer.devicecheck.appattest-environment")
unless app_attest_environments.include?("production")
UI.user_error!(
"Provisioning profile #{target.fetch("profileName")} for #{target.fetch("bundleId")} is missing production App Attest entitlement; actual environments: #{app_attest_environments.empty? ? "missing" : app_attest_environments.join(", ")}."
)
end
end
end
def sync_app_store_signing!(readonly:)

View File

@@ -65,7 +65,7 @@ pnpm ios:release:signing:check
pnpm ios:release:signing:setup
```
`signing:setup` uses Fastlane `produce` and `modify_services` to create Developer Portal bundle IDs and enable required services before running `match`. The main app and share extension also require the shared App Group from `apps/ios/Config/AppStoreSigning.json`; associate that group with both bundle IDs in the Apple Developer Portal before regenerating profiles. If Fastlane does not already have a valid Apple Developer Portal session, run `fastlane spaceauth` for a release-owner Apple ID and export the resulting `FASTLANE_SESSION`.
`signing:setup` uses Fastlane `produce` and `modify_services` to create Developer Portal bundle IDs and enable required services before running `match`. The main app also requires App Attest, and the main app and share extension both require the shared App Group from `apps/ios/Config/AppStoreSigning.json`; associate that group with both bundle IDs in the Apple Developer Portal before regenerating profiles. If Fastlane does not already have a valid Apple Developer Portal session, run `fastlane spaceauth` for a release-owner Apple ID and export the resulting `FASTLANE_SESSION`.
Shared encrypted signing storage:

View File

@@ -96,6 +96,7 @@ cat >"${tmp_file}" <<EOF
// OPENCLAW_IOS_WATCH_APP_PROFILE
OPENCLAW_CODE_SIGN_STYLE = ${code_sign_style}
OPENCLAW_CODE_SIGN_IDENTITY = ${code_sign_identity}
OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClaw.entitlements
OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
// Keep legacy key for compatibility with older signing config paths.
OPENCLAW_IOS_SELECTED_TEAM = ${team_id}

View File

@@ -185,12 +185,16 @@ OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClawAppAttest.entitlements
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = production
OPENCLAW_APP_ATTEST_ENVIRONMENT = production
OPENCLAW_PUSH_TRANSPORT = relay
OPENCLAW_PUSH_DISTRIBUTION = official
OPENCLAW_URL_SLASH = /
OPENCLAW_PUSH_RELAY_BASE_URL = ${PUSH_RELAY_BASE_URL_XCCONFIG}
OPENCLAW_PUSH_APNS_ENVIRONMENT = production
OPENCLAW_PUSH_RELAY_PROFILE = production
OPENCLAW_PUSH_PROOF_POLICY = appleStrict
EOF
(

View File

@@ -15,23 +15,126 @@ XCODEGEN_BIN="${IOS_RUN_XCODEGEN_BIN:-xcodegen}"
SIMCTL_BIN="${IOS_RUN_SIMCTL_BIN:-xcrun simctl}"
PLIST_BUDDY_BIN="${IOS_RUN_PLIST_BUDDY_BIN:-/usr/libexec/PlistBuddy}"
usage() {
cat <<'EOF'
Usage: scripts/ios-run.sh [options]
Options:
--push-sandbox-simulator
Build with the hosted sandbox push relay and launch the simulator with an
internal simulator proof secret.
--push-relay-base-url <url>
Override the sandbox relay URL used with --push-sandbox-simulator.
Defaults to https://ios-push-relay-sandbox.openclaw.ai.
--simulator-proof-secret-env <name>
Environment variable that contains the simulator proof secret.
Defaults to OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET.
-h, --help
Show this help.
EOF
}
run_simctl() {
# shellcheck disable=SC2086
${SIMCTL_BIN} "$@"
}
push_sandbox_simulator=0
push_relay_base_url="${OPENCLAW_PUSH_SANDBOX_RELAY_BASE_URL:-https://ios-push-relay-sandbox.openclaw.ai}"
simulator_proof_secret_env="${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET_ENV:-OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET}"
while [[ $# -gt 0 ]]; do
case "$1" in
--push-sandbox-simulator)
push_sandbox_simulator=1
;;
--push-relay-base-url)
if [[ $# -lt 2 || -z "$2" ]]; then
echo "ERROR: --push-relay-base-url requires a URL" >&2
exit 1
fi
push_relay_base_url="$2"
shift
;;
--simulator-proof-secret-env)
if [[ $# -lt 2 || -z "$2" ]]; then
echo "ERROR: --simulator-proof-secret-env requires an environment variable name" >&2
exit 1
fi
simulator_proof_secret_env="$2"
shift
;;
-h | --help)
usage
exit 0
;;
*)
echo "ERROR: Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
xcodebuild_overrides=()
simulator_proof_secret=""
if [[ ! "${simulator_proof_secret_env}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "ERROR: Invalid simulator proof secret environment variable name" >&2
exit 1
fi
if [[ "${push_sandbox_simulator}" == "1" ]]; then
simulator_proof_secret="${!simulator_proof_secret_env:-}"
if [[ -z "${simulator_proof_secret}" ]]; then
echo "ERROR: ${simulator_proof_secret_env} must be set for --push-sandbox-simulator" >&2
exit 1
fi
if [[ "${#simulator_proof_secret}" -lt 32 ]]; then
echo "ERROR: ${simulator_proof_secret_env} must contain at least 32 characters" >&2
exit 1
fi
xcodebuild_overrides+=(
"OPENCLAW_PUSH_TRANSPORT=relay"
"OPENCLAW_PUSH_DISTRIBUTION=official"
"OPENCLAW_PUSH_RELAY_BASE_URL=${push_relay_base_url}"
"OPENCLAW_PUSH_APNS_ENVIRONMENT=sandbox"
"OPENCLAW_PUSH_RELAY_PROFILE=simulatorSandbox"
"OPENCLAW_PUSH_PROOF_POLICY=internalSimulator"
"OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT=development"
)
fi
unset "${simulator_proof_secret_env}"
if [[ "${simulator_proof_secret_env}" != "OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET" ]]; then
unset OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET
fi
"${ROOT_DIR}/scripts/ios-configure-signing.sh"
"${ROOT_DIR}/scripts/ios-write-version-xcconfig.sh"
cd "${IOS_DIR}"
"${XCODEGEN_BIN}" generate
"${XCODEBUILD_BIN}" \
-project OpenClaw.xcodeproj \
-scheme OpenClaw \
-destination "${IOS_DESTINATION}" \
-configuration "${CONFIGURATION}" \
-derivedDataPath "${DERIVED_DATA_DIR}" \
build
if [[ "${push_sandbox_simulator}" == "1" ]]; then
"${XCODEBUILD_BIN}" \
-project OpenClaw.xcodeproj \
-scheme OpenClaw \
-destination "${IOS_DESTINATION}" \
-configuration "${CONFIGURATION}" \
-derivedDataPath "${DERIVED_DATA_DIR}" \
build \
"${xcodebuild_overrides[@]}"
else
"${XCODEBUILD_BIN}" \
-project OpenClaw.xcodeproj \
-scheme OpenClaw \
-destination "${IOS_DESTINATION}" \
-configuration "${CONFIGURATION}" \
-derivedDataPath "${DERIVED_DATA_DIR}" \
build
fi
app_path="${DERIVED_DATA_DIR}/Build/Products/${CONFIGURATION}-iphonesimulator/${APP_NAME}.app"
if [[ ! -d "${app_path}" ]]; then
@@ -54,4 +157,10 @@ if ! boot_output="$(run_simctl boot "${SIMULATOR_TARGET}" 2>&1)"; then
fi
run_simctl install "${SIMULATOR_TARGET}" "${app_path}"
run_simctl launch "${SIMULATOR_TARGET}" "${bundle_id}"
if [[ "${push_sandbox_simulator}" == "1" ]]; then
# shellcheck disable=SC2086
SIMCTL_CHILD_OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET="${simulator_proof_secret}" \
${SIMCTL_BIN} launch "${SIMULATOR_TARGET}" "${bundle_id}"
else
run_simctl launch "${SIMULATOR_TARGET}" "${bundle_id}"
fi

View File

@@ -64,6 +64,7 @@ describe.sequential("scripts/ios-configure-signing.sh", () => {
expect(stdout).toContain("team=FWJYW4S8P8 app=ai.openclawfoundation.app");
expect(generated).toContain("OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8");
expect(generated).toContain("OPENCLAW_CODE_SIGN_ENTITLEMENTS = Sources/OpenClaw.entitlements");
expect(generated).toContain("OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app");
expect(generated).toContain("OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share");
expect(generated).toContain("OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared");

View File

@@ -79,7 +79,7 @@ describe("scripts/ios-release-signing.mjs", () => {
expect(output).toContain("Signing branch: main");
expect(output).toContain("Signing setup and sync: Fastlane match");
expect(output).not.toContain("OpenClawWatchExtension");
expect(output).toContain("capabilities: PUSH_NOTIFICATIONS, APP_GROUPS");
expect(output).toContain("capabilities: PUSH_NOTIFICATIONS, APP_GROUPS, APP_ATTEST");
expect(output).toContain("app groups: group.ai.openclawfoundation.app.shared");
});
});

View File

@@ -39,12 +39,18 @@ function makeFixture(bundleId: string): { root: string; script: string; logFile:
path.join(scriptsDir, "ios-configure-signing.sh"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" || -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then
printf 'configure-signing-proof-env leaked\\n' >>"${logFile}"
fi
`,
);
writeExecutable(
path.join(scriptsDir, "ios-write-version-xcconfig.sh"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" || -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then
printf 'write-version-proof-env leaked\\n' >>"${logFile}"
fi
`,
);
writeExecutable(
@@ -52,6 +58,12 @@ set -euo pipefail
`#!/usr/bin/env bash
set -euo pipefail
printf 'xcodegen %s\\n' "$*" >>"${logFile}"
if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then
printf 'xcodegen-proof-env leaked\\n' >>"${logFile}"
fi
if [[ -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then
printf 'xcodegen-custom-proof-env leaked\\n' >>"${logFile}"
fi
`,
);
writeExecutable(
@@ -59,6 +71,12 @@ printf 'xcodegen %s\\n' "$*" >>"${logFile}"
`#!/usr/bin/env bash
set -euo pipefail
printf 'xcodebuild %s\\n' "$*" >>"${logFile}"
if [[ -n "\${OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then
printf 'xcodebuild-proof-env leaked\\n' >>"${logFile}"
fi
if [[ -n "\${CUSTOM_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then
printf 'xcodebuild-custom-proof-env leaked\\n' >>"${logFile}"
fi
derived=""
configuration="Debug"
while [[ $# -gt 0 ]]; do
@@ -90,6 +108,13 @@ PLIST
`#!/usr/bin/env bash
set -euo pipefail
printf 'simctl %s\\n' "$*" >>"${logFile}"
if [[ "$1" == "launch" ]]; then
if [[ -n "\${SIMCTL_CHILD_OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET:-}" ]]; then
printf 'simctl-launch-proof set\\n' >>"${logFile}"
else
printf 'simctl-launch-proof unset\\n' >>"${logFile}"
fi
fi
if [[ "$1" == "boot" ]]; then
if [[ "\${SIMCTL_BOOT_MODE:-}" == "booted" ]]; then
echo "Unable to boot device in current state: Booted" >&2
@@ -113,8 +138,12 @@ sed -n 's:.*<key>CFBundleIdentifier</key><string>\\([^<]*\\)</string>.*:\\1:p' "
return { root, script, logFile };
}
function runIosRun(fixture: { root: string; script: string }, extraEnv = {}): string {
return execFileSync(BASH_BIN, bashArgs(fixture.script), {
function runIosRun(
fixture: { root: string; script: string },
extraEnv = {},
args: string[] = [],
): string {
return execFileSync(BASH_BIN, [...bashArgs(fixture.script), ...args], {
env: {
...process.env,
IOS_DERIVED_DATA_DIR: path.join(fixture.root, "DerivedData"),
@@ -146,6 +175,54 @@ describe("scripts/ios-run.sh", () => {
);
});
it("builds simulator sandbox relay mode and injects proof secret only at launch", () => {
const fixture = makeFixture("ai.openclawfoundation.app");
const proofSecret = "x".repeat(32);
runIosRun(
fixture,
{
OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET: proofSecret,
SIMCTL_BOOT_MODE: "booted",
},
["--push-sandbox-simulator"],
);
const log = readFileSync(fixture.logFile, "utf8");
expect(log).toContain("OPENCLAW_PUSH_TRANSPORT=relay");
expect(log).toContain("OPENCLAW_PUSH_DISTRIBUTION=official");
expect(log).toContain(
"OPENCLAW_PUSH_RELAY_BASE_URL=https://ios-push-relay-sandbox.openclaw.ai",
);
expect(log).toContain("OPENCLAW_PUSH_APNS_ENVIRONMENT=sandbox");
expect(log).toContain("OPENCLAW_PUSH_RELAY_PROFILE=simulatorSandbox");
expect(log).toContain("OPENCLAW_PUSH_PROOF_POLICY=internalSimulator");
expect(log).toContain("simctl launch iPhone 17 ai.openclawfoundation.app");
expect(log).toContain("simctl-launch-proof set");
expect(log).not.toContain("proof-env leaked");
expect(log).not.toContain(proofSecret);
});
it("scrubs exported simulator proof secrets from normal build helpers", () => {
const fixture = makeFixture("ai.openclawfoundation.app");
const proofSecret = "x".repeat(32);
const customProofSecret = "y".repeat(32);
runIosRun(fixture, {
OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET: proofSecret,
OPENCLAW_SIMULATOR_PUSH_PROOF_SECRET_ENV: "CUSTOM_SIMULATOR_PUSH_PROOF_SECRET",
CUSTOM_SIMULATOR_PUSH_PROOF_SECRET: customProofSecret,
SIMCTL_BOOT_MODE: "booted",
});
const log = readFileSync(fixture.logFile, "utf8");
expect(log).toContain("simctl launch iPhone 17 ai.openclawfoundation.app");
expect(log).toContain("simctl-launch-proof unset");
expect(log).not.toContain("proof-env leaked");
expect(log).not.toContain(proofSecret);
expect(log).not.toContain(customProofSecret);
});
it("does not ignore simulator boot failures other than already booted", () => {
const fixture = makeFixture("ai.openclawfoundation.app");