IntentGate is a macOS prototype for approving high-impact software-agent actions with a YubiKey 5.8. Instead of asking a person to approve a generic sign-in, it binds the approval to the exact operation the software is about to perform.
The example in this repository is a vendor payment. A local dashboard creates the request, the native signer displays the amount and destination, and a YubiKey signs the SHA-256 digest of the canonical payment manifest using previewSign. The simulated payment is released only if the signed request still matches the final execution payload.
Authentication answers “which credential was used?” It does not automatically answer “what did the person agree to?” That difference becomes important when an agent can prepare actions with real consequences.
I built IntentGate to make that boundary visible. The normal path shows an approved operation being released. The integrity-test path changes the amount after approval: the YubiKey credential and signature are still valid, but the release is blocked because the final operation is no longer the one that was reviewed.
I kept the upstream side deterministic on purpose. The dashboard stands in for an invoice agent, but the demo does not need a live model or an external payment API. That let me spend the hackathon on the authorization and verification flow rather than on prompt behavior or third-party setup.
- macOS 14 or newer
- Xcode 16.3 or newer (the included packages use Swift tools 6.1)
- Python 3.10 or newer
- A YubiKey 5.8 that reports
previewSignsupport
The Swift dependencies are included under Vendor/. Installing the Python dependency is the only setup step that needs network access.
From the project root:
python3 -m venv IntentGateServer/.venv
IntentGateServer/.venv/bin/python -m pip install -r IntentGateServer/requirements.txt- Open
IntentGate.xcodeprojin Xcode. - Select the
IntentGateSignerscheme and the My Mac destination. - Press Run.
- In IntentGate, press Start Server, then Dashboard.
- Select the YubiKey 5.8 marked
previewSign: availableand press Register YubiKey. Enter its FIDO PIN and touch it when prompted. - Create a payment in the dashboard, return to the signer, review it, and press Approve & Sign.
When several YubiKeys are connected, IntentGate lists all FIDO HID devices and binds the ceremony to the one you selected.
- Leave Change the final amount off.
- Create the default
$8,400.00payment in the dashboard. - Review the amount, destination, invoice, expiry, and manifest hash in the signer.
- Approve the request and touch the selected YubiKey.
- The receipt shows that the WebAuthn credential,
previewSignsignature, and final payload checks passed.
- Reset the dashboard.
- Turn on Change the final amount.
- Create and approve the same payment.
- IntentGate changes the simulated execution amount from
$8,400.00to$84,000.00after approval. - The receipt shows a valid credential and signature, but different approved and execution hashes, so the release is blocked.
This second path is the main idea behind the project: a valid signature is only useful for the operation it actually covers.
sequenceDiagram
participant Dashboard as Web dashboard
participant Server as IntentGate server
participant Signer as macOS signer
participant Key as YubiKey 5.8
Dashboard->>Server: Create canonical payment manifest
Signer->>Server: Poll for a pending request
Server-->>Signer: Manifest, WebAuthn challenge, and ARKG arguments
Signer->>Signer: Re-encode and hash the displayed manifest
Signer->>Key: WebAuthn assertion + previewSign digest
Key-->>Signer: Credential response + operation signature
Signer->>Server: Submit the approval response
Server->>Server: Verify credential, signature, expiry, nonce, and final payload
Server-->>Dashboard: Release or block with a receipt
The server encodes each PaymentManifest as canonical JSON and stores those exact bytes and their SHA-256 hash. Before opening a YubiKey ceremony, the signer decodes the bytes for display, re-encodes the manifest, and verifies that the displayed model and server-provided hash still agree.
The release gate checks all of the following:
- The registered WebAuthn credential produced the assertion.
- The assertion contains a
previewSignresult and its signature verifies with the ARKG-derived public key. - The signed digest belongs to the canonical payment manifest shown in the signer.
- The request has not expired or already been completed.
- The final execution payload has the same canonical bytes and hash as the approved manifest.
- Only then is the simulated payment marked as released.
The browser is not treated as an identity provider, and IntentGate does not claim to prove who originally requested the payment. Its job is to show the request and the release decision. The YubiKey ceremony proves that the registered signer approved the exact manifest.
The supplied macOS FIDO UI connected to the first available HID device. I added a narrow device-selection path so IntentGate can enumerate connected keys, show their firmware and capabilities, and open the selected device for every later ceremony.
The identifier available through macOS HID is a location ID, not a YubiKey serial number. Moving a key to a different USB port can therefore make it appear to be a different device. IntentGate fails closed in that case and asks you to reconnect or register a different key. The exact changes to the supplied packages are documented in Vendor/README.md.
IntentGate/
├── IntentGate.xcodeproj/ Supported Xcode build
├── Sources/IntentGateSigner/ SwiftUI signer, API client, and YubiKey flow
├── IntentGateServer/server/ Local relying party, release gate, and dashboard
├── IntentGateServer/Tests/ Python unit and HTTP smoke tests
├── Resources/ App Info.plist and entitlements
├── Vendor/ Included YubiKit and FidoUI source + licenses
└── Package.swift Optional SwiftPM build of the signer target
The Xcode project is the normal way to run the app. Package.swift points at the same local source packages and provides a second reproducible compile path.
The signer starts the Python service as a child process and stops it when the app quits normally. It does not install a launch agent or background daemon. If the app is force-quit, the next launch only cleans up a process that matches the IntentGate server command and project folder; an unrelated process using port 8787 is left alone and reported as a conflict.
For server-only debugging, run:
cd IntentGateServer
.venv/bin/python -m server.app --host 127.0.0.1 --port 8787The API and dashboard are then available at http://127.0.0.1:8787.
IntentGate is a local hackathon prototype, not a payment product.
- The payment executor is simulated; no money moves and no payment provider is contacted.
- Requests, enrollment, and receipts are stored in memory and disappear when the server stops.
- The relying-party configuration is for a local demonstration (
localhostand127.0.0.1). - The flow follows the user-presence model in the supplied quickstart. It is not a complete account or workforce identity system.
- There is no durable audit log, remote attestation policy, multi-user approval, or production policy engine.
previewSignis a YubiKey 5.8 preview API. This project explores an authorization pattern; it does not define a production protocol around that API.
Recreate the local Python environment:
python3 -m venv IntentGateServer/.venv
IntentGateServer/.venv/bin/python -m pip install -r IntentGateServer/requirements.txtIf port 8787 is already occupied by another program, IntentGate reports the conflict instead of terminating that process.
Connect the YubiKey over USB, press Refresh, and choose a device marked previewSign: available. Older firmware may appear in the list but cannot complete the operation-signing ceremony.
IntentGate uses the YubiKit, FidoUI, and Python FIDO2 work provided by Yubico for the YubiKey 5.8 developer program. The included YubiKit and FidoUI licenses and the small project-specific modifications are documented under Vendor/.