evals/evals.json
{
"skill_name": "permissionkit",
"evals": [
{
"id": 0,
"name": "availability-imessage-review",
"prompt": "Review this PermissionKit setup for an app that supports iOS 26.0 and iPadOS 26.0: the team uses PermissionButton and AskCenter everywhere without availability checks, says PermissionKit can route child approval through our own in-app DM system, and wants to add a made-up PermissionKit entitlement. Write the corrected implementation guidance.",
"expected_output": "A concise PermissionKit setup review that distinguishes 26.0/26.1/26.2 API availability, preserves the iMessage-only communication-experience limitation, and avoids invented entitlement requirements.",
"files": [],
"assertions": [
"States that core PermissionKit topic, handle, question, response, choice, and CommunicationLimits APIs are available starting in iOS/iPadOS 26.0.",
"States that AskError starts in iOS/iPadOS 26.1 and AskCenter, AskCenter ask/responses APIs, PermissionButton, and SignificantAppUpdateTopic require iOS/iPadOS 26.2 or appropriate availability checks.",
"Says PermissionKit communication experiences are available only through iMessage and should not be presented as an arbitrary in-app DM routing framework.",
"Rejects inventing a PermissionKit entitlement key and says to verify current Apple docs/Xcode capabilities before adding signing requirements.",
"Keeps the answer focused on PermissionKit rather than turning it into a general chat moderation or family-account architecture."
]
},
{
"id": 1,
"name": "known-handle-response-flow",
"prompt": "Fix this PermissionKit flow: before showing an Ask button we call `isKnownHandle(_:)` and treat `false` as proof that communication limits are enabled, then call `AskCenter.shared.ask` without handling `communicationLimitsNotEnabled`, and our UI waits forever for a PermissionResponse after every tap. Give corrected Swift-level guidance and pseudocode.",
"expected_output": "A corrected PermissionKit communication flow that separates known-handle lookup from enabled-limits handling, catches AskError cases, tracks pending questions, and accounts for child cancellation.",
"files": [],
"assertions": [
"Explains that isKnownHandle(_:) and knownHandles(in:) classify system-known handles and do not prove communication limits are enabled.",
"Mentions that knownHandles(in:) requires a non-nil, nonempty app bundle identifier.",
"Handles AskError.communicationLimitsNotEnabled from AskCenter.shared.ask as the normal fallback when communication limits are not active.",
"Uses AskCenter.shared.responses(for: CommunicationTopic.self) to observe parent decisions rather than deprecated CommunicationLimits ask/update APIs.",
"Models pending, retry, cancellation, or expiration because child cancellation of the iMessage send flow may produce no PermissionResponse."
]
},
{
"id": 2,
"name": "significant-update-boundary",
"prompt": "A children’s app wants to use `SignificantAppUpdateTopic(description: \"We improved the app\")` on devices running iOS 26.0, skip response observation, and use the same flow for every terms-of-service wording tweak. Review the plan and provide the minimal corrected PermissionKit guidance.",
"expected_output": "A significant-app-update PermissionKit review that guards iOS/iPadOS 26.2 APIs, requires concrete descriptions, explains the developer/regulatory significance decision, and observes responses.",
"files": [],
"assertions": [
"States that SignificantAppUpdateTopic and the matching AskCenter ask/responses flow require iOS/iPadOS 26.2+ availability.",
"Says the app developer determines whether an update is significant based on applicable regulations rather than using the topic for every small wording tweak.",
"Replaces vague descriptions like \"We improved the app\" with concise, understandable text describing concrete user-visible changes.",
"Observes AskCenter.shared.responses(for: SignificantAppUpdateTopic.self) and handles approval, denial, and missing response states.",
"Keeps legal/regulatory discussion at the boundary and does not provide jurisdiction-specific legal advice."
]
}
]
}
references/permissionkit-patterns.md
# PermissionKit Extended Patterns
Overflow reference for the `permissionkit` skill. Contains advanced patterns
that exceed the main skill file's scope.
## Contents
- [Full UIKit Integration](#full-uikit-integration)
- [Response Observer Manager](#response-observer-manager)
- [Multi-Contact Permission Flow](#multi-contact-permission-flow)
- [Communication Limits Checking Pattern](#communication-limits-checking-pattern)
- [SwiftUI Full-Screen Permission Flow](#swiftui-full-screen-permission-flow)
- [macOS Integration](#macos-integration)
- [Error Recovery Patterns](#error-recovery-patterns)
## Full UIKit Integration
Complete UIKit view controller with permission request and response handling.
Keep the pending state explicit: if the child cancels the iMessage send flow,
PermissionKit does not deliver a `PermissionResponse` for that question.
```swift
import UIKit
import PermissionKit
class ContactViewController: UIViewController {
private var responseTask: Task<Void, Never>?
override func viewDidLoad() {
super.viewDidLoad()
startObservingResponses()
}
deinit {
responseTask?.cancel()
}
func requestPermissionToMessage(_ contact: Contact) {
let personInfo = CommunicationTopic.PersonInformation(
handle: CommunicationHandle(
value: contact.phoneNumber,
kind: .phoneNumber
),
nameComponents: contact.nameComponents,
avatarImage: contact.avatarCGImage
)
let topic = CommunicationTopic(
personInformation: [personInfo],
actions: [.message]
)
let question = PermissionQuestion<CommunicationTopic>(
communicationTopic: topic
)
Task {
do {
try await AskCenter.shared.ask(question, in: self)
showPendingState(for: contact)
schedulePendingExpiration(for: question.id)
} catch AskError.communicationLimitsNotEnabled {
enableMessaging(for: contact)
} catch AskError.notAvailable {
showFeatureUnavailable()
} catch {
showError(error)
}
}
}
private func startObservingResponses() {
responseTask = Task { [weak self] in
let responses = AskCenter.shared.responses(
for: CommunicationTopic.self
)
for await response in responses {
await MainActor.run {
self?.handleResponse(response)
}
}
}
}
@MainActor
private func handleResponse(_ response: PermissionResponse<CommunicationTopic>) {
switch response.choice.answer {
case .approval:
let handles = response.question.topic.personInformation
.map(\.handle)
for handle in handles {
enableCommunication(for: handle)
}
case .denial:
let handles = response.question.topic.personInformation
.map(\.handle)
for handle in handles {
showDeniedState(for: handle)
}
@unknown default:
break
}
}
private func showPendingState(for contact: Contact) { }
private func schedulePendingExpiration(for id: UUID) { }
private func enableMessaging(for contact: Contact) { }
private func enableCommunication(for handle: CommunicationHandle) { }
private func showDeniedState(for handle: CommunicationHandle) { }
private func showFeatureUnavailable() { }
private func showError(_ error: Error) { }
}
```
## Response Observer Manager
Centralize response observation for apps with multiple permission flows.
```swift
import PermissionKit
@Observable
@MainActor
final class PermissionManager {
static let shared = PermissionManager()
var approvedHandles: Set<String> = []
var deniedHandles: Set<String> = []
var pendingHandleValues: Set<String> = []
var pendingQuestionIDs: Set<UUID> = []
private var observerTask: Task<Void, Never>?
private init() {
startObserving()
}
deinit {
observerTask?.cancel()
}
func askPermission(
for handles: [CommunicationHandle],
actions: Set<CommunicationTopic.Action>,
in viewController: UIViewController
) async throws {
let personInfo = handles.map { handle in
CommunicationTopic.PersonInformation(
handle: handle,
nameComponents: nil,
avatarImage: nil
)
}
let topic = CommunicationTopic(
personInformation: personInfo,
actions: actions
)
let question = PermissionQuestion<CommunicationTopic>(
communicationTopic: topic
)
try await AskCenter.shared.ask(question, in: viewController)
let handleValues = handles.map(\.value)
pendingQuestionIDs.insert(question.id)
pendingHandleValues.formUnion(handleValues)
schedulePendingExpiration(
for: question.id,
handleValues: handleValues
)
}
func isApproved(_ handleValue: String) -> Bool {
approvedHandles.contains(handleValue)
}
func isDenied(_ handleValue: String) -> Bool {
deniedHandles.contains(handleValue)
}
func isPending(_ handleValue: String) -> Bool {
pendingHandleValues.contains(handleValue)
}
private func startObserving() {
observerTask = Task { [weak self] in
let responses = AskCenter.shared.responses(
for: CommunicationTopic.self
)
for await response in responses {
await MainActor.run {
self?.processResponse(response)
}
}
}
}
private func processResponse(
_ response: PermissionResponse<CommunicationTopic>
) {
pendingQuestionIDs.remove(response.question.id)
let handleValues = response.question.topic.personInformation
.map(\.handle.value)
for value in handleValues {
pendingHandleValues.remove(value)
}
switch response.choice.answer {
case .approval:
for value in handleValues {
approvedHandles.insert(value)
deniedHandles.remove(value)
}
case .denial:
for value in handleValues {
deniedHandles.insert(value)
}
@unknown default:
break
}
}
private func schedulePendingExpiration(
for id: UUID,
handleValues: [String]
) {
// Expire or offer retry if no response arrives after your product's
// chosen pending window. Child cancellation produces no response.
}
}
```
## Multi-Contact Permission Flow
Request permission for multiple contacts in a single question.
```swift
func requestGroupPermission(
contacts: [Contact],
in viewController: UIViewController
) async throws {
let personInfoList = contacts.map { contact in
CommunicationTopic.PersonInformation(
handle: CommunicationHandle(
value: contact.identifier,
kind: .custom
),
nameComponents: contact.nameComponents,
avatarImage: contact.avatarCGImage
)
}
let topic = CommunicationTopic(
personInformation: personInfoList,
actions: [.message, .audioCall, .videoCall]
)
let question = PermissionQuestion<CommunicationTopic>(
communicationTopic: topic
)
// Check question properties
print("Question ID: \(question.id)")
print("Choices: \(question.choices.map(\.title))")
print("Default choice: \(question.defaultChoice.title)")
if let expiration = question.expirationDate {
print("Expires: \(expiration)")
}
try await AskCenter.shared.ask(question, in: viewController)
}
```
## Communication Limits Checking Pattern
Check which handles are already known to the system before building the
permission UI. This does not prove communication limits are enabled; still
handle `AskError.communicationLimitsNotEnabled` when asking. `knownHandles(in:)`
requires a non-nil, nonempty app bundle identifier.
```swift
@Observable
@MainActor
final class ContactListViewModel {
var contacts: [ContactItem] = []
struct ContactItem: Identifiable {
let id: String
let name: String
let handle: CommunicationHandle
var isKnownBySystem: Bool = false
var needsPermissionPrompt: Bool = false
}
func refreshContactStatus() async {
guard Bundle.main.bundleIdentifier?.isEmpty == false else { return }
let limits = CommunicationLimits.current
let allHandles = Set(contacts.map(\.handle))
let knownHandles = await limits.knownHandles(in: allHandles)
for i in contacts.indices {
contacts[i].isKnownBySystem = knownHandles.contains(
contacts[i].handle
)
contacts[i].needsPermissionPrompt = !contacts[i].isKnownBySystem
}
}
}
```
## SwiftUI Full-Screen Permission Flow
Build a complete permission flow in SwiftUI.
```swift
import SwiftUI
import PermissionKit
struct ContactDetailView: View {
let contact: Contact
@State private var permissionState: PermissionState = .unknown
@Environment(PermissionManager.self) private var permissionManager
enum PermissionState {
case unknown, checking, needsPermission, approved, denied
case pending, error(String)
}
var body: some View {
VStack {
Text(contact.name)
.font(.title)
switch permissionState {
case .unknown, .checking:
ProgressView("Checking permissions...")
case .needsPermission:
let handle = CommunicationHandle(
value: contact.phoneNumber,
kind: .phoneNumber
)
let question = PermissionQuestion<CommunicationTopic>(
handle: handle
)
VStack {
Text("Permission needed to message this contact.")
.foregroundStyle(.secondary)
PermissionButton(question: question) {
Label("Ask to Message", systemImage: "message.badge.clock")
}
.buttonStyle(.borderedProminent)
}
case .pending:
Label("Waiting for parent response", systemImage: "clock")
.foregroundStyle(.secondary)
case .approved:
Label("Messaging enabled", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
case .denied:
Label("Permission denied", systemImage: "xmark.circle.fill")
.foregroundStyle(.red)
case .error(let message):
Label(message, systemImage: "exclamationmark.triangle")
.foregroundStyle(.orange)
}
}
.task {
await checkPermission()
}
}
private func checkPermission() async {
permissionState = .checking
let handle = CommunicationHandle(
value: contact.phoneNumber,
kind: .phoneNumber
)
let limits = CommunicationLimits.current
let isKnown = await limits.isKnownHandle(handle)
if isKnown {
permissionState = .approved
} else if permissionManager.isApproved(contact.phoneNumber) {
permissionState = .approved
} else if permissionManager.isDenied(contact.phoneNumber) {
permissionState = .denied
} else if permissionManager.isPending(contact.phoneNumber) {
permissionState = .pending
} else {
permissionState = .needsPermission
}
}
}
```
## macOS Integration
On macOS 26.2+, pass an `NSWindow` instead of `UIViewController`.
```swift
#if os(macOS)
import AppKit
import PermissionKit
func requestPermission(
for question: PermissionQuestion<CommunicationTopic>,
in window: NSWindow
) async throws {
try await AskCenter.shared.ask(question, in: window)
}
#endif
```
## Error Recovery Patterns
Provide actionable recovery for each error type.
```swift
func handleAskError(_ error: AskError) -> (title: String, message: String, action: (() -> Void)?) {
switch error {
case .communicationLimitsNotEnabled:
return (
"No Restrictions",
"Communication limits are not enabled. You can communicate freely.",
nil
)
case .contactSyncNotSetup:
return (
"Contact Sync Required",
"Please enable contact sync in Settings to use this feature.",
{ openContactSyncSettings() }
)
case .invalidQuestion:
return (
"Invalid Request",
"The permission request could not be created. Please try again.",
nil
)
case .notAvailable:
return (
"Not Available",
"This feature is not available on this device.",
nil
)
case .systemError(let underlying):
return (
"System Error",
underlying.localizedDescription,
nil
)
case .unknown:
return (
"Unknown Error",
"An unexpected error occurred. Please try again later.",
nil
)
@unknown default:
return (
"Error",
"An error occurred.",
nil
)
}
}
```
SKILL.md
---
name: permissionkit
description: "Create child communication safety experiences using PermissionKit to request parental permission for children. Use when building apps that involve child-to-contact communication, need to check communication limits, request parent/guardian approval, or handle permission responses for minors."
---
# PermissionKit
Request permission from a parent or guardian to modify a child's communication
rules. PermissionKit creates communication safety experiences that let children ask for exceptions to communication limits set by their parents.
PermissionKit communication experiences are available only through iMessage.
Use it for parent/guardian approval flows, not as a general in-app contact
permission, moderation, or chat-safety framework.
## Contents
- [Availability and Setup](#availability-and-setup)
- [Core Concepts](#core-concepts)
- [Checking Communication Limits](#checking-communication-limits)
- [Creating Permission Questions](#creating-permission-questions)
- [Requesting Permission with AskCenter](#requesting-permission-with-askcenter)
- [SwiftUI Integration with PermissionButton](#swiftui-integration-with-permissionbutton)
- [Handling Responses](#handling-responses)
- [Significant App Update Topic](#significant-app-update-topic)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Availability and Setup
Import `PermissionKit`. Do not invent PermissionKit entitlement keys; verify
current Apple documentation and Xcode capabilities before adding signing
requirements.
```swift
import PermissionKit
```
Use this centralized version matrix and verify it against the current SDK:
| Tier | APIs | iOS/iPadOS/Mac Catalyst/macOS/visionOS |
|---|---|---|
| Core | Topics, handles, questions, responses, choices, `CommunicationLimits` | 26.0+ |
| Errors | `AskError` | 26.1+ |
| Presentation | `AskCenter`, ask/response sequences, `PermissionButton`, significant-update topics | 26.2+ |
## Core Concepts
PermissionKit manages a flow where:
1. A child encounters a communication limit in your app
2. Your app creates a `PermissionQuestion` describing the request
3. The system presents the question to the child for them to send to their parent
4. The parent reviews and approves or denies the request
5. Your app receives a `PermissionResponse` with the parent's decision
### Key Types
| Type | Role |
|---|---|
| `AskCenter` | Singleton that manages permission requests and responses |
| `PermissionQuestion` | Describes the permission being requested |
| `PermissionResponse` | The parent's decision (approval or denial) |
| `PermissionChoice` | The specific answer (approve/decline) |
| `PermissionButton` | SwiftUI button that triggers the permission flow |
| `CommunicationTopic` | Topic for communication-related permission requests |
| `CommunicationHandle` | A phone number, email, or custom identifier |
| `CommunicationLimits` | Checks which communication handles are known to the system |
| `SignificantAppUpdateTopic` | Topic for significant app update permission requests |
## Checking Communication Limits
Use `CommunicationLimits.current` to check whether the system already knows a
communication handle for your app. This is not an "are communication limits
enabled?" probe. If limits are not enabled, `AskCenter.shared.ask(_:in:)`
throws `AskError.communicationLimitsNotEnabled`; handle that path when asking.
`knownHandles(in:)` also requires the calling app to have a non-nil, nonempty
bundle identifier. Corrected code should guard `Bundle.main.bundleIdentifier`
before calling it.
```swift
import PermissionKit
func needsPermissionPrompt(for handle: CommunicationHandle) async -> Bool {
let limits = CommunicationLimits.current
let isKnown = await limits.isKnownHandle(handle)
return !isKnown
}
// Check multiple handles at once.
func filterKnownHandles(_ handles: Set<CommunicationHandle>) async -> Set<CommunicationHandle> {
guard Bundle.main.bundleIdentifier?.isEmpty == false else { return [] }
let limits = CommunicationLimits.current
return await limits.knownHandles(in: handles)
}
```
### Creating Communication Handles
```swift
let phoneHandle = CommunicationHandle(
value: "+1234567890",
kind: .phoneNumber
)
let emailHandle = CommunicationHandle(
value: "friend@example.com",
kind: .emailAddress
)
let customHandle = CommunicationHandle(
value: "user123",
kind: .custom
)
```
## Creating Permission Questions
Build a `PermissionQuestion` with the contact information and communication
action type.
```swift
// Question for a single contact
let handle = CommunicationHandle(value: "+1234567890", kind: .phoneNumber)
let question = PermissionQuestion<CommunicationTopic>(handle: handle)
// Question for multiple contacts
let handles = [
CommunicationHandle(value: "+1234567890", kind: .phoneNumber),
CommunicationHandle(value: "friend@example.com", kind: .emailAddress)
]
let multiQuestion = PermissionQuestion<CommunicationTopic>(handles: handles)
```
### Using CommunicationTopic with Person Information
Provide display names and avatars for a richer permission prompt.
```swift
let personInfo = CommunicationTopic.PersonInformation(
handle: CommunicationHandle(value: "+1234567890", kind: .phoneNumber),
nameComponents: {
var name = PersonNameComponents()
name.givenName = "Alex"
name.familyName = "Smith"
return name
}(),
avatarImage: nil
)
let topic = CommunicationTopic(
personInformation: [personInfo],
actions: [.message, .audioCall]
)
let question = PermissionQuestion<CommunicationTopic>(communicationTopic: topic)
```
### Communication Actions
| Action | Description |
|---|---|
| `.message` | Text messaging |
| `.audioCall` | Voice call |
| `.videoCall` | Video call |
| `.call` | Generic call |
| `.chat` | Chat communication |
| `.follow` | Follow a user |
| `.beFollowed` | Allow being followed |
| `.friend` | Friend request |
| `.connect` | Connection request |
| `.communicate` | Generic communication |
## Requesting Permission with AskCenter
Use `AskCenter.shared` to request that the child send the permission question
to their parent or guardian. The async `ask` call starts the send flow; parent
decisions arrive later through `responses(for:)`. If the child cancels the send
flow, the system does not deliver a `PermissionResponse` for that question.
```swift
import PermissionKit
func requestPermission(
for question: PermissionQuestion<CommunicationTopic>,
in viewController: UIViewController
) async {
do {
try await AskCenter.shared.ask(question, in: viewController)
// Question send flow was started; wait for responses(for:) separately.
} catch let error as AskError {
switch error {
case .communicationLimitsNotEnabled:
// Communication limits not active -- continue with normal app flow.
break
case .contactSyncNotSetup:
// Contact sync not configured
break
case .invalidQuestion:
// Question is malformed
break
case .notAvailable:
// PermissionKit not available on this device
break
case .systemError(let underlying):
print("System error: \(underlying)")
case .unknown:
break
@unknown default:
break
}
}
}
```
## SwiftUI Integration with PermissionButton
`PermissionButton` is a SwiftUI view that triggers the permission flow when
tapped. It uses the same response model as `AskCenter`: observe responses and
model a pending/canceled state instead of assuming every tap produces a parent
decision.
```swift
import SwiftUI
import PermissionKit
struct ContactPermissionView: View {
let handle = CommunicationHandle(value: "+1234567890", kind: .phoneNumber)
var body: some View {
let question = PermissionQuestion<CommunicationTopic>(handle: handle)
PermissionButton(question: question) {
Label("Ask to Message", systemImage: "message")
}
}
}
```
For richer SwiftUI flows, custom topics, and long-lived managers, read
[references/permissionkit-patterns.md](references/permissionkit-patterns.md).
## Handling Responses
Listen for permission responses asynchronously. Track pending questions by
`question.id`, and give the UI a retry or expiration path because a child can
cancel the iMessage send flow without producing a response.
When combining known-handle checks with response handling, carry forward the
bundle-identifier guard from `knownHandles(in:)`.
```swift
enum PermissionRequestState {
case pending, approved, denied, expired
}
var requestStates: [UUID: PermissionRequestState] = [:]
func expireIfStillPending(_ id: UUID) {
guard requestStates[id] == .pending else { return }
requestStates[id] = .expired
// Re-enable asking or show retry/canceled UI.
}
func observeResponses() async {
let responses = AskCenter.shared.responses(for: CommunicationTopic.self)
for await response in responses {
let choice = response.choice
let question = response.question
switch choice.answer {
case .approval:
// Parent approved -- enable communication
requestStates[question.id] = .approved
print("Approved for topic: \(question.topic)")
case .denial:
// Parent denied -- keep restriction
requestStates[question.id] = .denied
print("Denied")
@unknown default:
break
}
}
}
```
### PermissionChoice Properties
```swift
let choice: PermissionChoice = response.choice
print("Answer: \(choice.answer)") // .approval or .denial
print("Choice ID: \(choice.id)")
print("Title: \(choice.title)")
// Convenience statics
let approved = PermissionChoice.approve
let declined = PermissionChoice.decline
```
## Significant App Update Topic
Request permission for significant app updates that require parental approval.
Your app determines what counts as significant based on applicable regulations
and should consult qualified legal counsel for compliance interpretation.
Use concise, understandable descriptions that state the concrete change parents
are approving.
```swift
let updateTopic = SignificantAppUpdateTopic(
description: "This update adds multiplayer chat features"
)
let question = PermissionQuestion<SignificantAppUpdateTopic>(
significantAppUpdateTopic: updateTopic
)
// Present the question
try await AskCenter.shared.ask(question, in: viewController)
requestStates[question.id] = .pending
scheduleExpiration(for: question.id)
// Listen for responses
for await response in AskCenter.shared.responses(for: SignificantAppUpdateTopic.self) {
switch response.choice.answer {
case .approval:
// Proceed with update
requestStates[response.question.id] = .approved
case .denial:
// Skip update
requestStates[response.question.id] = .denied
@unknown default:
break
}
}
// If no response arrives before your pending window expires, keep the update
// blocked or offer a retry. Child cancellation produces no denial response.
```
## Common Mistakes
| Mistake | Fix |
|---|---|
| Known-handle lookup is treated as proof that limits are enabled | Handle `.communicationLimitsNotEnabled` from the ask operation as the normal unconfigured path. |
| `AskError` is collapsed into one message | Distinguish limits-disabled, contact-sync, invalid-question, unavailable, system, and unknown cases. |
| Question has no handle or person information | Validate at least one meaningful communication target before presentation. |
| Ask is fire-and-forget | Observe response and pending state, while allowing child cancellation/abandonment. |
| Deprecated `CommunicationLimitsButton` is used | Use `PermissionButton`. |
## Review Checklist
- [ ] iMessage-only routing understood before choosing PermissionKit
- [ ] The centralized availability matrix is applied to every API in use
- [ ] `CommunicationHandle` created with correct `Kind` (phone, email, custom)
- [ ] Known-handle examples guard a non-nil, nonempty bundle identifier before
`knownHandles(in:)`
- [ ] Person information includes name components for a clear permission prompt
- [ ] Communication actions match the app's actual communication capabilities
- [ ] Response handling updates UI on the main actor
- [ ] Error states provide clear guidance to the user
## References
- Extended patterns (response handling, multi-topic, UIKit): [references/permissionkit-patterns.md](references/permissionkit-patterns.md)
- [PermissionKit framework](https://sosumi.ai/documentation/permissionkit)
- [AskCenter](https://sosumi.ai/documentation/permissionkit/askcenter)
- [PermissionQuestion](https://sosumi.ai/documentation/permissionkit/permissionquestion)
- [PermissionButton](https://sosumi.ai/documentation/permissionkit/permissionbutton)
- [PermissionResponse](https://sosumi.ai/documentation/permissionkit/permissionresponse)
- [CommunicationTopic](https://sosumi.ai/documentation/permissionkit/communicationtopic)
- [CommunicationHandle](https://sosumi.ai/documentation/permissionkit/communicationhandle)
- [CommunicationLimits](https://sosumi.ai/documentation/permissionkit/communicationlimits)
- [SignificantAppUpdateTopic](https://sosumi.ai/documentation/permissionkit/significantappupdatetopic)
- [AskError](https://sosumi.ai/documentation/permissionkit/askerror)
- [Creating a communication experience](https://sosumi.ai/documentation/permissionkit/creating-a-communication-experience)