shareplay-activities — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited shareplay-activities (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 2 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Build shared real-time experiences using the GroupActivities framework. SharePlay connects people over FaceTime, Messages, AirDrop, and nearby visionOS sharing, synchronizing media playback, app state, or custom data. Targets Swift 6.3 / iOS 26+.
Add the Group Activities capability to the app target in Xcode. Xcode adds the required entitlement and updates the provisioning profile:
<key>com.apple.developer.group-session</key>
<true/>Configure this only for app targets. Group Activities are not available in widgets, extensions, or App Clips.
import GroupActivities
let observer = GroupStateObserver()
// Check if a FaceTime call or Messages conversation is active
if observer.isEligibleForGroupSession {
showSharePlayButton()
}Observe changes reactively:
for await isEligible in observer.$isEligibleForGroupSession.values {
showSharePlayButton(isEligible)
}Conform to GroupActivity and provide metadata:
import GroupActivities
struct WatchTogetherActivity: GroupActivity {
let movieID: String
let movieTitle: String
var metadata: GroupActivityMetadata {
var meta = GroupActivityMetadata()
meta.title = movieTitle
meta.type = .watchTogether
meta.fallbackURL = URL(string: "https://example.com/movie/\(movieID)")
return meta
}
}| Type | Use Case |
|---|---|
.generic | Default for custom activities |
.watchTogether | Video playback |
.listenTogether | Audio playback |
.createTogether | Collaborative creation (drawing, editing) |
.exploreTogether | Shared browsing, planning, or exploration |
.learnTogether | Shared learning or studying |
.readTogether | Shared reading |
.shopTogether | Shared shopping |
.workoutTogether | Shared fitness sessions |
GroupActivity is Codable; stored activity data must be codable. Add Transferable only for SwiftUI ShareLink, SharePlay over AirDrop, or AppKit/UIKit share sheets. Keep payloads minimal: use identifiers or URLs instead of large data.
Set up a long-lived task to receive sessions when another participant starts the activity:
@Observable
@MainActor
final class SharePlayManager {
private var session: GroupSession<WatchTogetherActivity>?
private var messenger: GroupSessionMessenger?
private var sessionTasks: [Task<Void, Never>] = []
func observeSessions() {
Task {
for await session in WatchTogetherActivity.sessions() {
self.configureSession(session)
}
}
}
private func configureSession(
_ session: GroupSession<WatchTogetherActivity>
) {
self.session = session
self.messenger = GroupSessionMessenger(session: session)
// Observe session state changes
let stateTask = Task {
for await state in session.$state.values {
handleState(state)
}
}
sessionTasks.append(stateTask)
// Observe participant changes
let participantTask = Task {
for await participants in session.$activeParticipants.values {
handleParticipants(participants)
}
}
sessionTasks.append(participantTask)
// Join the session
session.join()
}
private func cleanUp() {
sessionTasks.forEach { $0.cancel() }
sessionTasks.removeAll()
session = nil
messenger = nil
}
}| State | Description |
|---|---|
.waiting | Session exists but local participant has not joined |
.joined | Local participant is actively in the session |
.invalidated(reason:) | Session ended (check reason for details) |
private func handleState(_ state: GroupSession<WatchTogetherActivity>.State) {
switch state {
case .waiting:
print("Waiting to join")
case .joined:
print("Joined session")
loadActivity(session?.activity)
case .invalidated(let reason):
print("Session ended: \(reason)")
cleanUp()
@unknown default:
break
}
}
private func handleParticipants(_ participants: Set<Participant>) {
print("Active participants: \(participants.count)")
}// Leave the session (other participants continue)
session?.leave()
// End the session for all participants
session?.end()Use GroupSessionMessenger to sync small, time-sensitive app state between participants.
Messages must be Codable; keep each message under 256 KB.
struct SyncMessage: Codable {
let action: String
let timestamp: Date
let data: [String: String]
}func sendSync(_ message: SyncMessage) async throws {
guard let messenger else { return }
try await messenger.send(message, to: .all)
}
// Send to specific participants
try await messenger.send(message, to: .only(participant))func observeMessages() {
guard let messenger else { return }
Task {
for await (message, context) in messenger.messages(of: SyncMessage.self) {
let sender = context.source
handleReceivedMessage(message, from: sender)
}
}
}// Reliable (default) -- checked and retried for crucial state
let reliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .reliable
)
// Unreliable -- lower latency, no delivery guarantee
let unreliableMessenger = GroupSessionMessenger(
session: session,
deliveryMode: .unreliable
)Use .reliable for state-changing actions such as selections or turns. Use .unreliable for high-frequency ephemeral data such as cursor positions, drawing strokes, and reactions.
For video/audio, use AVPlaybackCoordinator with AVPlayer:
import AVFoundation
import GroupActivities
func configurePlayback(
session: GroupSession<WatchTogetherActivity>,
player: AVPlayer
) {
// Connect the player's coordinator to the session
let coordinator = player.playbackCoordinator
coordinator.coordinateWithSession(session)
}Once connected, AVFoundation synchronizes play/pause, seeking, rate, playback speed, and time. Do not put AVPlayer transport fields in messenger messages or snapshots, including late-joiner snapshots; use custom messages only for state outside playback.
import GroupActivities
import UIKit
func startSharePlay() async throws {
let activity = WatchTogetherActivity(
movieID: "123",
movieTitle: "Great Movie"
)
switch await activity.prepareForActivation() {
case .activationPreferred:
// A conversation is active and the user chose to share.
_ = try await activity.activate()
case .activationDisabled:
// The user chose local playback, or sharing is unavailable.
startLocalExperience()
case .cancelled:
break
@unknown default:
break
}
}When no conversation is active (i.e., isEligibleForGroupSession is false), use GroupActivitySharingController to let the user pick contacts first:
let controller = try GroupActivitySharingController(activity)
present(controller, animated: true)Use the shareplay SF Symbol for custom controls. Treat GroupActivityMetadata as discovery copy: concise title, subtitle, image, and type aligned with the entry point. Keep sibling domains out: GameKit owns auth, matchmaking, leaderboards, achievements, and voice/chat; TabletopKit owns seats, board equipment, spatial placement, turns, rules, and authoritative tabletop state; AVKit owns playback UI. SharePlay owns invitations, lifecycle, participants, and coordination handoffs. See references/shareplay-patterns.md for SwiftUI ShareLink, AirDrop, and direct activation patterns.
For larger, non-time-sensitive attachments, use GroupSessionJournal instead of GroupSessionMessenger. Journal items must conform to Transferable, are available to late joiners, and are limited to 100 MB. It requires iOS/iPadOS/tvOS 17+, macOS 14+, or visionOS 1+. For larger/protected assets, share a pointer or manifest and use server storage or app-managed file transfer.
import GroupActivities
let journal = GroupSessionJournal(session: session)
// Upload a Transferable file or data item
let attachment = try await journal.add(sharedImageItem)
// Observe incoming attachments
Task {
for await attachments in journal.attachments {
for attachment in attachments {
let data = try await attachment.load(Data.self)
handleReceivedFile(data)
}
}
}// WRONG -- session is received but never joined
for await session in MyActivity.sessions() {
self.session = session
// Session stays in .waiting state forever
}
// CORRECT -- join after configuring
for await session in MyActivity.sessions() {
self.session = session
self.messenger = GroupSessionMessenger(session: session)
session.join()
}// WRONG -- session stays alive after the user navigates away
func viewDidDisappear() {
// Nothing -- session leaks
}
// CORRECT -- leave when the view is dismissed
func viewDidDisappear() {
session?.leave()
session = nil
messenger = nil
}// WRONG -- broadcasting state without handling late joiners
func onJoin() {
// New participant has no idea what the current state is
}
// CORRECT -- send full state to new participants
func handleParticipants(_ participants: Set<Participant>) {
let newParticipants = participants.subtracting(knownParticipants)
for participant in newParticipants {
Task {
try await messenger?.send(currentState, to: .only(participant))
}
}
knownParticipants = participants
}// WRONG -- messenger is small/time-sensitive; journal is Transferable and <=100 MB
let imageData = try Data(contentsOf: imageURL) // 300 KB
try await messenger.send(imageData, to: .all) // Too large
// CORRECT -- journal attachments up to 100 MB; otherwise share a pointer/manifest
let journal = GroupSessionJournal(session: session)
try await journal.add(sharedImageItem)
// Larger/protected assets: server storage or app-managed file transfer// WRONG -- manually syncing play/pause when using AVPlayer
func play() {
player.play()
try await messenger.send(PlayMessage(), to: .all)
}
// CORRECT -- let AVPlaybackCoordinator handle it
player.playbackCoordinator.coordinateWithSession(session)
player.play() // Automatically synced to all participants// WRONG -- each time the view appears, a new listener is created
struct MyView: View {
var body: some View {
Text("Hello")
.task {
for await session in MyActivity.sessions() { }
}
}
}
// CORRECT -- observe sessions in a long-lived manager
@Observable
final class ActivityManager {
init() {
Task {
for await session in MyActivity.sessions() {
configureSession(session)
}
}
}
}GroupActivity struct is Codable with meaningful metadataTransferable conformance added when using ShareLink, AirDrop, or share sheetssessions() observed in a long-lived object (not a SwiftUI view body)session.join() called after receiving and configuring the sessionsession.leave() called when the user navigates away or dismissesGroupSessionMessenger messages stay under 256 KB with appropriate deliveryMode$state and $activeParticipants publishers observed for lifecycle changesGroupSessionJournal used for non-time-sensitive Transferable attachmentsAVPlaybackCoordinator used for media sync (not manual messages)GroupStateObserver.isEligibleForGroupSession checked before showing SharePlay UIGroupActivitySharingController used when no conversation is active~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.