fix(ios): prettify bonjour endpoint labels
This commit is contained in:
@@ -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
95
docs/gateway/pairing.md
Normal 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.
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user