fix(ios): wire share extension app group signing

This commit is contained in:
joshavant 2026-06-19 12:53:33 +02:00
parent 2983edd5a2
commit f7f415f26b
No known key found for this signature in database
GPG key ID: 4463B60B0DD49BC4
20 changed files with 137 additions and 8 deletions

View file

@ -3,6 +3,7 @@
"signingRepo": "git@github.com:openclaw/apps-signing.git",
"signingBranch": "main",
"profileType": "appstore",
"appGroupId": "group.ai.openclawfoundation.app.shared",
"targets": [
{
"target": "OpenClaw",
@ -11,7 +12,8 @@
"platform": "IOS",
"profileKey": "OPENCLAW_APP_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app",
"capabilities": ["PUSH_NOTIFICATIONS"]
"capabilities": ["PUSH_NOTIFICATIONS", "APP_GROUPS"],
"appGroups": ["group.ai.openclawfoundation.app.shared"]
},
{
"target": "OpenClawShareExtension",
@ -20,7 +22,8 @@
"platform": "IOS",
"profileKey": "OPENCLAW_SHARE_PROFILE",
"profileName": "OpenClaw App Store ai.openclawfoundation.app.share",
"capabilities": []
"capabilities": ["APP_GROUPS"],
"appGroups": ["group.ai.openclawfoundation.app.shared"]
},
{
"target": "OpenClawActivityWidget",

View file

@ -7,6 +7,7 @@ OPENCLAW_DEVELOPMENT_TEAM = $(OPENCLAW_IOS_SELECTED_TEAM)
OPENCLAW_CODE_SIGN_STYLE = Automatic
OPENCLAW_CODE_SIGN_IDENTITY = Apple Development
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
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_ACTIVITY_WIDGET_PROFILE =

View file

@ -7,6 +7,7 @@ OPENCLAW_DEVELOPMENT_TEAM = YOUR_TEAM_ID
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp

View file

@ -101,6 +101,7 @@ Release-owner secrets:
- App Store Connect API auth uses Keychain for private key material plus non-secret `apps/ios/fastlane/.env` variables.
- The encrypted signing repo password lives outside this repo in the release-owner vault and is exposed locally as `MATCH_PASSWORD`.
- The share sheet requires the Apple Developer App Group in `apps/ios/Config/AppStoreSigning.json` to be associated with both the app and share-extension bundle IDs before App Store profiles are regenerated.
- Apple Distribution private keys, certificates, provisioning profiles, and decrypted signing sync output stay under `apps/ios/build/` or Keychain and are gitignored.
- Rotating release signing means refreshing Fastlane `match` assets and pushing a fresh encrypted sync state.
@ -156,6 +157,8 @@ This should create `apps/ios/fastlane/.env` with non-secret App Store Connect va
- `ai.openclawfoundation.app.activitywidget`
- `ai.openclawfoundation.app.watchkitapp`
The main app and share extension must both be associated with the App Group pinned in `apps/ios/Config/AppStoreSigning.json`.
Use `pnpm ios:release:signing:setup` for the initial portal setup, then `MATCH_PASSWORD=... pnpm ios:release:signing:sync:push` to publish encrypted Fastlane match assets to the shared private repo.
4. Optional: set a custom official relay URL for the build. If unset, the release flow uses `https://ios-push-relay.openclaw.ai`.

View file

@ -41,5 +41,7 @@
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
<key>OpenClawAppGroupIdentifier</key>
<string>$(OPENCLAW_APP_GROUP_ID)</string>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>$(OPENCLAW_APP_GROUP_ID)</string>
</array>
</dict>
</plist>

View file

@ -10,6 +10,7 @@ OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8
OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app
OPENCLAW_SHARE_BUNDLE_ID = ai.openclawfoundation.app.share
OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared
OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclawfoundation.app.watchkitapp
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclawfoundation.app.activitywidget
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = development

View file

@ -78,6 +78,8 @@
<string>OpenClaw uses on-device speech recognition for talk mode and voice wake.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>OpenClawAppGroupIdentifier</key>
<string>$(OPENCLAW_APP_GROUP_ID)</string>
<key>OpenClawCanonicalVersion</key>
<string>$(OPENCLAW_IOS_VERSION)</string>
<key>OpenClawPushAPNsEnvironment</key>

View file

@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>$(OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT)</string>
<key>com.apple.security.application-groups</key>
<array>
<string>$(OPENCLAW_APP_GROUP_ID)</string>
</array>
</dict>
</plist>

View file

@ -3,6 +3,10 @@ import OpenClawKit
import Testing
@Suite struct ShareToAgentDeepLinkTests {
@Test func appGroupIdentifierUsesCanonicalOpenClawGroup() {
#expect(OpenClawAppGroup.canonicalIdentifier == "group.ai.openclawfoundation.app.shared")
}
@Test func buildMessageIncludesSharedFields() {
let payload = SharedContentPayload(
title: "Article",

View file

@ -492,6 +492,9 @@ def produce_services_for_target(target)
if target.fetch("capabilities").include?("PUSH_NOTIFICATIONS")
services[:push_notification] = "on"
end
if target.fetch("capabilities").include?("APP_GROUPS")
services[:app_group] = "on"
end
services
end
@ -567,6 +570,15 @@ def profile_plist_value(profile_path, key_path)
end
end
def profile_plist_array_values(profile_path, key_path)
raw = profile_plist_value(profile_path, key_path)
return [] unless raw
raw.lines.map(&:strip).reject do |line|
line.empty? || line == "Array {" || line == "}"
end
end
def validate_match_profile_capabilities!(target)
capabilities = target.fetch("capabilities")
return if capabilities.empty?
@ -582,6 +594,17 @@ def validate_match_profile_capabilities!(target)
)
end
end
if capabilities.include?("APP_GROUPS")
expected_app_groups = target.fetch("appGroups")
actual_app_groups = profile_plist_array_values(profile_path, "Entitlements:com.apple.security.application-groups")
missing = expected_app_groups - actual_app_groups
unless missing.empty?
UI.user_error!(
"Provisioning profile #{target.fetch("profileName")} for #{target.fetch("bundleId")} is missing App Groups #{missing.join(", ")}; actual groups: #{actual_app_groups.empty? ? "missing" : actual_app_groups.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`. 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 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`.
Shared encrypted signing storage:

View file

@ -142,6 +142,7 @@ targets:
- openclaw
CFBundleShortVersionString: "$(OPENCLAW_MARKETING_VERSION)"
OpenClawCanonicalVersion: "$(OPENCLAW_IOS_VERSION)"
OpenClawAppGroupIdentifier: "$(OPENCLAW_APP_GROUP_ID)"
CFBundleVersion: "$(OPENCLAW_BUILD_VERSION)"
UILaunchScreen: {}
UIApplicationSceneManifest:
@ -194,6 +195,7 @@ targets:
settings:
base:
CODE_SIGN_IDENTITY: "$(OPENCLAW_CODE_SIGN_IDENTITY)"
CODE_SIGN_ENTITLEMENTS: ShareExtension/OpenClawShareExtension.entitlements
CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)"
DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)"
ENABLE_APPINTENTS_METADATA: NO
@ -208,6 +210,7 @@ targets:
properties:
CFBundleDisplayName: OpenClaw Share
CFBundleShortVersionString: "$(OPENCLAW_MARKETING_VERSION)"
OpenClawAppGroupIdentifier: "$(OPENCLAW_APP_GROUP_ID)"
CFBundleVersion: "$(OPENCLAW_BUILD_VERSION)"
NSExtension:
NSExtensionPointIdentifier: com.apple.share-services

View file

@ -0,0 +1,11 @@
import Foundation
public enum OpenClawAppGroup {
public static let canonicalIdentifier = "group.ai.openclawfoundation.app.shared"
public static var identifier: String {
let raw = Bundle.main.object(forInfoDictionaryKey: "OpenClawAppGroupIdentifier") as? String
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? self.canonicalIdentifier : trimmed
}
}

View file

@ -26,7 +26,7 @@ public struct ShareGatewayRelayConfig: Codable, Sendable, Equatable {
}
public enum ShareGatewayRelaySettings {
private static let suiteName = "group.ai.openclaw.shared"
private static var suiteName: String { OpenClawAppGroup.identifier }
private static let relayConfigKey = "share.gatewayRelay.config.v1"
private static let lastEventKey = "share.gatewayRelay.event.v1"

View file

@ -1,7 +1,7 @@
import Foundation
public enum ShareToAgentSettings {
private static let suiteName = "group.ai.openclaw.shared"
private static var suiteName: String { OpenClawAppGroup.identifier }
private static let defaultInstructionKey = "share.defaultInstruction"
private static var defaults: UserDefaults {

View file

@ -68,6 +68,7 @@ fi
bundle_base="$(normalize_bundle_id "${bundle_base}")"
share_bundle_id="${OPENCLAW_IOS_SHARE_BUNDLE_ID:-${bundle_base}.share}"
app_group_id="${OPENCLAW_IOS_APP_GROUP_ID:-group.${bundle_base}.shared}"
activity_widget_bundle_id="${OPENCLAW_IOS_ACTIVITY_WIDGET_BUNDLE_ID:-${bundle_base}.activitywidget}"
watch_app_bundle_id="${OPENCLAW_IOS_WATCH_APP_BUNDLE_ID:-${bundle_base}.watchkitapp}"
@ -86,7 +87,8 @@ cat >"${tmp_file}" <<EOF
// Override values with env vars if needed:
// IOS_DEVELOPMENT_TEAM / IOS_PREFERRED_TEAM_ID
// OPENCLAW_IOS_APP_BUNDLE_ID / OPENCLAW_IOS_BUNDLE_ID_BASE
// OPENCLAW_IOS_SHARE_BUNDLE_ID / OPENCLAW_IOS_ACTIVITY_WIDGET_BUNDLE_ID
// OPENCLAW_IOS_SHARE_BUNDLE_ID / OPENCLAW_IOS_APP_GROUP_ID
// OPENCLAW_IOS_ACTIVITY_WIDGET_BUNDLE_ID
// OPENCLAW_IOS_WATCH_APP_BUNDLE_ID
// OPENCLAW_IOS_CODE_SIGN_STYLE / OPENCLAW_IOS_CODE_SIGN_IDENTITY
// OPENCLAW_IOS_APNS_ENTITLEMENT_ENVIRONMENT / OPENCLAW_IOS_APP_PROFILE
@ -99,6 +101,7 @@ OPENCLAW_DEVELOPMENT_TEAM = ${team_id}
OPENCLAW_IOS_SELECTED_TEAM = ${team_id}
OPENCLAW_APP_BUNDLE_ID = ${bundle_base}
OPENCLAW_SHARE_BUNDLE_ID = ${share_bundle_id}
OPENCLAW_APP_GROUP_ID = ${app_group_id}
OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ${activity_widget_bundle_id}
OPENCLAW_WATCH_APP_BUNDLE_ID = ${watch_app_bundle_id}
OPENCLAW_APNS_ENTITLEMENT_ENVIRONMENT = ${apns_entitlement_environment}

View file

@ -7,6 +7,15 @@ import { fileURLToPath } from "node:url";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultManifestPath = path.join(rootDir, "apps", "ios", "Config", "AppStoreSigning.json");
function validateAppGroupId(value, context) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`${context} must be a non-empty string.`);
}
if (!/^group\.[A-Za-z0-9.-]+$/.test(value)) {
throw new Error(`${context} must be an Apple app group identifier beginning with group.`);
}
}
function usage() {
process.stdout.write(`Usage:
scripts/ios-release-signing.mjs --mode plan
@ -59,6 +68,9 @@ function readManifest(manifestPath) {
if (!Array.isArray(parsed.targets) || parsed.targets.length === 0) {
throw new Error("Signing manifest must include targets.");
}
if (typeof parsed.appGroupId !== "undefined") {
validateAppGroupId(parsed.appGroupId, "Signing manifest appGroupId");
}
for (const target of parsed.targets) {
for (const key of [
@ -76,6 +88,40 @@ function readManifest(manifestPath) {
if (!Array.isArray(target.capabilities)) {
throw new Error(`Signing target ${target.target} must include capabilities array.`);
}
for (const capability of target.capabilities) {
if (typeof capability !== "string" || capability.trim() === "") {
throw new Error(`Signing target ${target.target} capabilities must be non-empty strings.`);
}
}
const appGroups = target.appGroups ?? [];
if (!Array.isArray(appGroups)) {
throw new Error(`Signing target ${target.target} appGroups must be an array when present.`);
}
for (const appGroup of appGroups) {
validateAppGroupId(appGroup, `Signing target ${target.target} appGroups entry`);
}
const hasAppGroupsCapability = target.capabilities.includes("APP_GROUPS");
if (hasAppGroupsCapability && appGroups.length === 0) {
throw new Error(
`Signing target ${target.target} must list appGroups when APP_GROUPS is enabled.`,
);
}
if (!hasAppGroupsCapability && appGroups.length > 0) {
throw new Error(
`Signing target ${target.target} lists appGroups without APP_GROUPS capability.`,
);
}
if (
typeof parsed.appGroupId === "string" &&
appGroups.length > 0 &&
!appGroups.includes(parsed.appGroupId)
) {
throw new Error(
`Signing target ${target.target} appGroups must include manifest appGroupId.`,
);
}
}
return parsed;
@ -86,6 +132,9 @@ function writeXcconfig(manifest) {
"OPENCLAW_CODE_SIGN_STYLE = Manual",
"OPENCLAW_CODE_SIGN_IDENTITY = Apple Distribution",
];
if (typeof manifest.appGroupId === "string") {
lines.push(`OPENCLAW_APP_GROUP_ID = ${manifest.appGroupId}`);
}
for (const target of manifest.targets) {
lines.push(`${target.profileKey} = ${target.profileName}`);
@ -106,8 +155,10 @@ Targets:
`);
for (const target of manifest.targets) {
const capabilities = target.capabilities.length > 0 ? target.capabilities.join(", ") : "none";
const appGroups =
target.appGroups?.length > 0 ? `, app groups: ${target.appGroups.join(", ")}` : "";
process.stdout.write(
`- ${target.target}: ${target.bundleId}, profile "${target.profileName}", capabilities: ${capabilities}\n`,
`- ${target.target}: ${target.bundleId}, profile "${target.profileName}", capabilities: ${capabilities}${appGroups}\n`,
);
}
}

View file

@ -25,6 +25,7 @@ function runConfigureSigning(teamId: string, user = "localuser"): string {
OPENCLAW_IOS_APP_BUNDLE_ID: "",
OPENCLAW_IOS_BUNDLE_ID_BASE: "",
OPENCLAW_IOS_BUNDLE_SUFFIX: "",
OPENCLAW_IOS_APP_GROUP_ID: "",
OPENCLAW_IOS_SHARE_BUNDLE_ID: "",
OPENCLAW_IOS_ACTIVITY_WIDGET_BUNDLE_ID: "",
OPENCLAW_IOS_WATCH_APP_BUNDLE_ID: "",
@ -65,6 +66,7 @@ describe.sequential("scripts/ios-configure-signing.sh", () => {
expect(generated).toContain("OPENCLAW_DEVELOPMENT_TEAM = FWJYW4S8P8");
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");
expect(generated).toContain("OPENCLAW_ACTIVITY_WIDGET_PROFILE = ");
});
@ -79,5 +81,8 @@ describe.sequential("scripts/ios-configure-signing.sh", () => {
expect(generated).toContain(
"OPENCLAW_APP_BUNDLE_ID = ai.openclawfoundation.app.test.localuser-y3yuzp442g",
);
expect(generated).toContain(
"OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.test.localuser-y3yuzp442g.shared",
);
});
});

View file

@ -18,6 +18,7 @@ describe("scripts/ios-release-signing.mjs", () => {
expect(output).toContain("OPENCLAW_CODE_SIGN_STYLE = Manual");
expect(output).toContain("OPENCLAW_CODE_SIGN_IDENTITY = Apple Distribution");
expect(output).toContain("OPENCLAW_APP_GROUP_ID = group.ai.openclawfoundation.app.shared");
expect(output).toContain("OPENCLAW_APP_PROFILE = OpenClaw App Store ai.openclawfoundation.app");
expect(output).toContain(
"OPENCLAW_SHARE_PROFILE = OpenClaw App Store ai.openclawfoundation.app.share",
@ -39,6 +40,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");
expect(output).toContain("capabilities: PUSH_NOTIFICATIONS, APP_GROUPS");
expect(output).toContain("app groups: group.ai.openclawfoundation.app.shared");
});
});