fix(ios): prettify bonjour endpoint labels

This commit is contained in:
Peter Steinberger
2025-12-13 02:48:06 +00:00
parent 21649d81d2
commit e95fdbbc37
3 changed files with 155 additions and 7 deletions

View File

@@ -53,7 +53,7 @@ final class BridgeDiscoveryModel: ObservableObject {
return DiscoveredBridge( return DiscoveredBridge(
name: name, name: name,
endpoint: result.endpoint, endpoint: result.endpoint,
debugID: String(describing: result.endpoint)) debugID: Self.prettyEndpointDebugID(result.endpoint))
default: default:
return nil return nil
} }
@@ -72,4 +72,37 @@ final class BridgeDiscoveryModel: ObservableObject {
self.bridges = [] self.bridges = []
self.statusText = "Stopped" self.statusText = "Stopped"
} }
private static func prettyEndpointDebugID(_ endpoint: NWEndpoint) -> String {
self.decodeBonjourEscapes(String(describing: endpoint))
}
private static func decodeBonjourEscapes(_ input: String) -> String {
// mDNS / DNS-SD commonly escapes spaces as `\\032` (decimal byte value 32). Make this human-friendly for UI.
var out = ""
var i = input.startIndex
while i < input.endIndex {
if input[i] == "\\",
let d0 = input.index(i, offsetBy: 1, limitedBy: input.index(before: input.endIndex)),
let d1 = input.index(i, offsetBy: 2, limitedBy: input.index(before: input.endIndex)),
let d2 = input.index(i, offsetBy: 3, limitedBy: input.index(before: input.endIndex)),
input[d0].isNumber,
input[d1].isNumber,
input[d2].isNumber
{
let digits = String(input[d0...d2])
if let value = Int(digits),
let scalar = UnicodeScalar(value)
{
out.append(Character(scalar))
i = input.index(i, offsetBy: 4)
continue
}
}
out.append(input[i])
i = input.index(after: i)
}
return out
}
} }

95
docs/gateway/pairing.md Normal file
View File

@@ -0,0 +1,95 @@
---
summary: "Gateway-owned node pairing (Option B) for iOS and other remote nodes"
read_when:
- Implementing node pairing approvals without macOS UI
- Adding CLI flows for approving remote nodes
- Extending gateway protocol with node management
---
# Gateway-owned pairing (Option B)
Goal: The Gateway (`clawd`) is the **source of truth** for which nodes are allowed to join the network.
This enables:
- Headless approval via terminal/CLI (no Swift UI required).
- Optional macOS UI approval (Swift app is just a frontend).
- One consistent membership store for iOS, mac nodes, future hardware nodes.
## Concepts
- **Pending request**: a node asked to join; requires explicit approve/reject.
- **Paired node**: node is allowed; gateway returns an auth token for subsequent connects.
- **Bridge**: LAN transport that forwards between node ↔ gateway. The bridge does not decide membership.
## API surface (gateway protocol)
These are conceptual method names; wire them into `src/gateway/protocol/schema.ts` and regenerate Swift types.
### Events
- `node.pair.requested`
- Emitted whenever a new pending pairing request is created.
- Payload:
- `requestId` (string)
- `nodeId` (string)
- `displayName?` (string)
- `platform?` (string)
- `version?` (string)
- `remoteIp?` (string)
- `ts` (ms since epoch)
- `node.pair.resolved`
- Emitted when a pending request is approved/rejected.
- Payload:
- `requestId` (string)
- `nodeId` (string)
- `decision` ("approved" | "rejected" | "expired")
- `ts` (ms since epoch)
### Methods
- `node.pair.request`
- Creates (or returns) a pending request.
- Params: node metadata (same shape as `node.pair.requested` payload, minus `requestId`/`ts`).
- Result:
- `requestId`
- `status` ("pending" | "alreadyPaired")
- If already paired: may include `token` directly to allow fast path.
- `node.pair.list`
- Returns:
- `pending[]` (pending requests)
- `paired[]` (paired node records)
- `node.pair.approve`
- Params: `{ requestId }`
- Result: `{ nodeId, token }`
- Must be idempotent (first decision wins).
- `node.pair.reject`
- Params: `{ requestId }`
- Result: `{ nodeId }`
## CLI flows
CLI must be able to fully operate without any GUI:
- `clawdis nodes pending`
- `clawdis nodes approve <requestId>`
- `clawdis nodes reject <requestId>`
Optional interactive helper:
- `clawdis nodes watch` (subscribe to `node.pair.requested` and prompt in-place)
## Storage (private, local)
Gateway stores the authoritative state under `~/.clawdis/`:
- `~/.clawdis/nodes/paired.json`
- `~/.clawdis/nodes/pending.json` (or `~/.clawdis/nodes/pending/*.json`)
Notes:
- Tokens are secrets. Treat `paired.json` as sensitive.
- Pending entries should have a TTL (e.g. 5 minutes) and expire automatically.
## Bridge integration
The macOS Bridge is responsible for:
- Surfacing the pairing request to the gateway (`node.pair.request`).
- Waiting for the decision (`node.pair.approve`/`reject`) and completing the on-wire pairing handshake to the node.
- Enforcing ACLs on what the node can call, even after paired.
The macOS UI (Swift) can:
- Subscribe to `node.pair.requested`, show an alert, and call `node.pair.approve` or `node.pair.reject`.
- Or ignore/dismiss (“Later”) and let CLI handle it.
## Implementation note
If the bridge is only provided by the macOS app, then “no Swift app running” cannot work end-to-end.
To support headless pairing, also add a `clawdis bridge` CLI mode that provides the Bonjour bridge service and forwards to the local gateway.

View File

@@ -51,11 +51,26 @@ Why:
- First connection: - First connection:
1) iOS generates a keypair (Secure Enclave if available). 1) iOS generates a keypair (Secure Enclave if available).
2) iOS connects to the bridge and requests pairing. 2) iOS connects to the bridge and requests pairing.
3) macOS app shows “Approve node” with node name + device metadata. 3) The bridge forwards the pairing request to the **Gateway** as a *pending request*.
4) On approve, mac stores the node public key + permissions; iOS stores bridge identity + trust anchor in Keychain. 4) Approval can happen via:
- **macOS UI** (Swift app shows “Approve node”), or
- **Terminal/CLI** (headless flows).
5) Once approved, the bridge returns a token to iOS; iOS stores it in Keychain.
- Subsequent connections: - Subsequent connections:
- The bridge requires the paired identity. Unpaired clients get a structured “not paired” error and no access. - The bridge requires the paired identity. Unpaired clients get a structured “not paired” error and no access.
#### Gateway-owned pairing (Option B details)
Pairing decisions must be owned by the Gateway (`clawd` / Node) so nodes can be approved without the macOS app running.
Key idea:
- The Swift app may still show an alert, but it is only a **frontend** for pending requests stored in the Gateway.
Desired behavior:
- If the Swift UI is present: show alert with Approve/Reject/Later.
- If the Swift UI is not present: `clawdis` CLI can list pending requests and approve/reject.
See `docs/gateway/pairing.md` for the API/events and storage.
### Authorization / scope control (bridge-side ACL) ### Authorization / scope control (bridge-side ACL)
The bridge must not be a raw proxy to every gateway method. The bridge must not be a raw proxy to every gateway method.
@@ -135,7 +150,7 @@ When iOS is backgrounded:
Create/expand SwiftPM targets so both apps share: Create/expand SwiftPM targets so both apps share:
- `ClawdisProtocol` (generated models; platform-neutral) - `ClawdisProtocol` (generated models; platform-neutral)
- `ClawdisGatewayClient` (shared WS framing + connect/req/res + seq-gap handling) - `ClawdisGatewayClient` (shared WS framing + connect/req/res + seq-gap handling)
- `ClawdisNodeKit` (node.invoke command types + error codes) - `ClawdisKit` (node/screen command types + deep links + shared utilities)
macOS continues to own: macOS continues to own:
- local Canvas implementation details (custom scheme handler serving on-disk HTML, window/panel presentation) - local Canvas implementation details (custom scheme handler serving on-disk HTML, window/panel presentation)
@@ -171,11 +186,16 @@ open ClawdisNode.xcodeproj
- `~/Library/Application Support/Clawdis/bridge/paired-nodes.json` - `~/Library/Application Support/Clawdis/bridge/paired-nodes.json`
- `~/Library/Application Support/Clawdis/bridge/keys/...` - `~/Library/Application Support/Clawdis/bridge/keys/...`
### Gateway (node)
- Pairing (source of truth):
- `~/.clawdis/nodes/paired.json`
- `~/.clawdis/nodes/pending.json` (or `pending/*.json` for auditability)
## Rollout plan (phased) ## Rollout plan (phased)
1) **Bridge discovery + pairing (mac + iOS)** 1) **Bridge discovery + pairing (mac + iOS)**
- Bonjour browse + resolve - Bonjour browse + resolve
- Approve prompt on mac - Approve prompt on mac
- Persist pairing in Keychain/App Support - Persist pairing in Keychain/App Support
2) **Voice-only node** 2) **Voice-only node**
- iOS voice wake toggle - iOS voice wake toggle
- Forward transcript to Gateway `agent` via bridge - Forward transcript to Gateway `agent` via bridge