All skills
garrytan avatar

/ios-qa

@730a101 official
by Garry Tangarrytan/gstack135k stars
20,051

Live-device iOS QA for SwiftUI apps. (gstack)

Use this Skill: https://skilld.dev/gh/garrytan/gstack/ios-qa

This session only. Nothing lands on disk.

docstailscale-acl-example.md

≈1.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Tailscale ACL example for the iOS QA daemon

The Mac-side daemon binds the Tailscale interface only when you pass --tailnet. By default the daemon is local-USB-only. This doc walks through the steps to expose your iPhone to remote agents safely so they can run iOS QA over the tailnet.

Threat model recap

  • iOS app StateServer: loopback-only always. Reachable from the Mac via the CoreDevice IPv6 tunnel. Never directly bound to tailnet.
  • Mac daemon: owns the tailnet interface. Binds two listeners — loopback (full surface, never forwarded) and tailnet (locked allowlist with capability tiers).
  • Auth: Tailscale identity validation via the local tailscaled socket (/var/run/tailscale.sock LocalAPI WhoIs). Allowlist file at ~/.gstack/ios-qa-allowlist.json is the single source of truth for who can do what.

Step 1: Install and run Tailscale

brew install --cask tailscale
# Login + start tailscaled, then verify:
tailscale status

Confirm the daemon can read the LocalAPI socket:

test -S /var/run/tailscale.sock && echo "socket present" || echo "MISSING"

If missing, the daemon will refuse to open the tailnet listener (fail-closed).

Step 2: Set up the daemon's ACL

The daemon needs to know which Tailscale identities are allowed to control which devices at which capability tier. The allowlist file is JSON:

{
  "version": 1,
  "entries": [
    {
      "identity": "you@example.com",
      "capabilities": ["restore"],
      "expires_at": null,
      "note": "Owner — full access"
    },
    {
      "identity": "ci@example.com",
      "capabilities": ["mutate"],
      "expires_at": "2026-12-31T00:00:00Z",
      "note": "CI runner — can write state but not full restore"
    },
    {
      "identity": "tag:claude-readonly",
      "capabilities": ["observe"],
      "expires_at": null,
      "note": "Agents that should only read"
    }
  ]
}

Identities are canonicalized via WhoIs:

  • User OAuth: user@example.com (no acct:, no domain rewriting).
  • Tagged nodes: tag:<tagname> (lowercased).
  • Node keys: node:<nodekey-hex> (rare; use tags instead).

Capability tiers are ordered: observe < interact < mutate < restore. Granting restore implies all lower tiers.

Step 3: Mint a session token for a remote agent

You can let agents self-mint (if their identity is allowlisted) or you can mint server-side for them:

# Server-side mint (owner-only, runs locally on the Mac with the device):
gstack-ios-qa-mint --remote ci@example.com --capability mutate --ttl 1h

# Self-service mint (agent over tailnet):
curl -X POST http://<mac-tailnet-ip>:9999/auth/mint \
  -H "Content-Type: application/json" \
  -d '{"capability": "interact"}'
# → {"session_token": "...", "expires_at": "...", "capability": "interact"}

Step 4: Tighten the Tailscale ACL (defense in depth)

The daemon's allowlist is the primary access control. Belt-and-suspenders: restrict the tailnet ACL to limit who can even reach the daemon port.

// In your tailscale admin console:
{
  "acls": [
    // Allow CI runner to reach the iOS QA Mac on port 9999 only.
    {
      "action": "accept",
      "src": ["ci@example.com"],
      "dst": ["ios-qa-mac:9999"]
    },
    // Tagged Claude agents — observe tier only (enforced by daemon, not ACL).
    {
      "action": "accept",
      "src": ["tag:claude-readonly"],
      "dst": ["ios-qa-mac:9999"]
    },
    // Default deny.
    {
      "action": "drop",
      "src": ["*"],
      "dst": ["ios-qa-mac:9999"]
    }
  ]
}

Step 5: Audit trail

Every authenticated mutating request through the tailnet listener writes a row to ~/.gstack/security/ios-qa-audit.jsonl:

{"ts":"2026-05-18T14:23:00Z","identity":"ci@example.com","device_udid":"00008101-XXXX","endpoint":"/tap","session_id":"abc...","capability":"interact","request_id":"req_001","status":200}

Rejections (no token, expired token, capability-insufficient, identity not allowlisted, rate limit hit) write to ~/.gstack/security/attempts.jsonl.

Rate limits

  • /auth/mint: 10 mints / 60s per identity. 11th returns 429.
  • Per-tailnet-request body: 1MB hard cap (413 above).
  • Screenshot response: 10MB hard cap (500 above with sanitized error).

Token lifetime

  • Daemon-minted session tokens: default 1h TTL, max 24h via --tailnet-session-ttl.
  • Refreshable via POST /session/heartbeat (extends by ttl_seconds, capped at the original max).
  • Boot token (between iOS app launch and daemon rotation): ~5s lifetime — daemon rotates immediately on first scrape.

Failure modes

Symptom Cause Action
Daemon refuses to open tailnet listener /var/run/tailscale.sock missing or permission-denied Install Tailscale; verify tailscale status works as the user running daemon
403 identity_not_allowed identity missing from allowlist Owner mint: gstack-ios-qa-mint --remote <identity>
403 capability_insufficient token tier below endpoint requirement Owner mint with higher --capability tier
429 rate_limited >10 mints/min from one identity Wait 60s; investigate why the agent is re-minting so often
409 schema_mismatch on /state/restore snapshot from older app build Discard the snapshot; re-capture from current app build

Source: SKILL.md on GitHub

1 warning3d3 checks · Risk SAFE
  • Gen Agent Trust Hub3d

    The ios-qa skill enables automated testing on physical iOS devices by deploying a debug bridge and using a vision-driven agent loop. It securely handles communication via a Mac-based daemon, featuring session management, capability-based access control (via Tailscale), and automated rotation of bootstrap credentials. The skill follows security best practices for secret management and uses trusted repositories for its development dependencies.

  • Socket3d

    3 alerts: gptAnomaly

  • Snyk3d

    Risk: LOW · No issues

Signed by skilld at 730a101. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated last week
What it can do
Runs commands Reads files Edits files
preamble-tier
3
version
1.0.0
All 7 allowed tools
BashReadWriteEditGrepGlobAskUserQuestion
Other metadata
triggers
[
  "ios qa",
  "test the iphone app",
  "test my ios app",
  "find bugs on the device",
  "qa the ios app"
]
  • Testing
  • swiftui
  • ios
  • qa
  • vision
  • device-testing
  • http-api
  • tailscale

README badge

README badge for garrytan/gstack/ios-qa

Connects a live iPhone via USB and runs a vision-driven QA loop—screenshot, analyze, decide, act, verify—against SwiftUI apps by reading source and interacting via HTTP to an embedded StateServer. Optionally exposes the device over Tailscale for remote agents. Use when asked to test an iOS app on a physical device or find bugs during live QA.

Generated from the current SKILL.md.

Does this skill work with real devices or simulators?
Only real iPhones connected via USB. It uses CoreDevice IPv6 tunneling to communicate with a StateServer embedded in the app under test.
What type of iOS apps can this test?
SwiftUI apps specifically. The skill reads Swift source to understand screen layouts and drives a vision-based test loop.
Can remote agents run iOS QA without physical access to the device?
Yes. The skill optionally exposes the device over Tailscale so remote agents like OpenClaw or Codex can run QA without touching the hardware.
What does the skill do in each test loop?
It captures a screenshot, analyzes it with vision, decides on the next action, interacts with the app via HTTP to the StateServer, and verifies the result before repeating.

Generated from the current SKILL.md. These answers refresh after source changes.