Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions RxCode/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3908,7 +3908,6 @@ final class AppState {
state.currentTurnOutputTokensUnkeyed = 0
}
broadcastMobileSessionStatus(sessionID: sessionKey, kind: .streamingStarted)
await permission.refreshRunToken()

let basePermissionMode = window.sessionPermissionMode ?? permissionMode
// Plan-mode boolean overrides the dropdown for the CLI `--permission-mode` flag only.
Expand Down Expand Up @@ -7259,8 +7258,6 @@ final class AppState {
state.currentTurnOutputTokensUnkeyed = 0
}

await permission.refreshRunToken()

let currentPermissionMode = sessionStates[sessionKey]?.permissionMode ?? permissionMode
let projectSelection = defaultModelSelection(for: projects.first { $0.id == projectId })
let agentProvider = sessionStates[sessionKey]?.agentProvider ?? projectSelection.provider
Expand Down
2 changes: 1 addition & 1 deletion RxCode/Services/GitHubService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Security

actor GitHubService {

static let oauthClientId = "Ov23liaj3hlJoMGsNZTW"
static let oauthClientId = "Ov23li0plkoiQCmLm5O5"
private let clientId = oauthClientId
private let logger = Logger(subsystem: "com.claudework", category: "GitHubService")
Comment on lines 6 to 10
private let sshKeyManager = SSHKeyManager()
Expand Down
43 changes: 27 additions & 16 deletions RxCode/Services/PermissionServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@ actor PermissionServer {
private static let basePort: UInt16 = 19836
private static let maxPort: UInt16 = 19846
private static let timeoutSeconds: UInt64 = 300 // 5 minutes
/// Upper bound on simultaneously-valid hook tokens. Each CLI launch mints one;
/// the oldest is evicted past this cap — generous enough that no realistic
/// number of concurrent agents ever loses a still-live token.
private static let maxValidRunTokens = 64

// MARK: - Properties

private var listener: NWListener?
private(set) var port: UInt16 = PermissionServer.basePort
private let appSecret = UUID().uuidString
private var runToken = UUID().uuidString
/// Hook tokens currently accepted on the `/hook/pre-tool-use` path. Each CLI
/// subprocess gets its own (see `mintRunToken`), so spawning a new agent never
/// invalidates the hook URL of an agent already running. Ordered oldest-first;
/// bounded by `maxValidRunTokens`.
private var validRunTokens: [String] = []
Comment on lines +28 to +32
private let logger = Logger(subsystem: "com.claudework", category: "PermissionServer")

/// Resolved outcome for a pending hook: the permission decision plus an optional
Expand Down Expand Up @@ -163,6 +171,7 @@ actor PermissionServer {
subscribers.removeAll()
sessionRegistry.removeAll()
sessionToolAllows.removeAll()
validRunTokens.removeAll()
bashCmdAllows = nil
}

Expand Down Expand Up @@ -248,7 +257,7 @@ actor PermissionServer {
id: toolUseId,
toolName: toolName,
toolInput: toolInput,
runToken: runToken,
runToken: "",
streamPermissionMode: mode,
sessionId: sessionId
)
Expand Down Expand Up @@ -305,20 +314,22 @@ actor PermissionServer {
return nil
}

/// Refresh the run token (call at the start of each CLI session).
func refreshRunToken() {
runToken = UUID().uuidString
}
// MARK: - Hook Settings

/// The current run token for building the hook URL.
func currentRunToken() -> String {
runToken
/// Mint a fresh run token and register it as valid. Each CLI subprocess gets
/// its own, so spawning a new agent never invalidates the hook URL of an agent
/// that is already running. Evicts the oldest token past `maxValidRunTokens`.
private func mintRunToken() -> String {
let token = UUID().uuidString
validRunTokens.append(token)
if validRunTokens.count > Self.maxValidRunTokens {
validRunTokens.removeFirst(validRunTokens.count - Self.maxValidRunTokens)
}
return token
}

// MARK: - Hook Settings

/// Generate the hook settings JSON that should be passed to `claude --settings`.
func generateHookSettings() -> String {
private func generateHookSettings(runToken: String) -> String {
let url = "http://127.0.0.1:\(port)/hook/pre-tool-use/\(appSecret)/\(runToken)"
let settings: [String: Any] = [
"hooks": [
Expand All @@ -343,9 +354,9 @@ actor PermissionServer {
return json
}

/// Write hook settings to a temporary file and return its path.
/// Mint a fresh run token, write hook settings to a temporary file, return its path.
func writeHookSettingsFile() throws -> String {
let json = generateHookSettings()
let json = generateHookSettings(runToken: mintRunToken())
let tempDir = FileManager.default.temporaryDirectory
let filePath = tempDir.appendingPathComponent("claudework-hooks-\(UUID().uuidString).json")
try json.write(to: filePath, atomically: true, encoding: .utf8)
Expand Down Expand Up @@ -373,7 +384,7 @@ actor PermissionServer {
components[0] == "hook",
components[1] == "pre-tool-use",
components[2] == appSecret,
components[3] == runToken else {
validRunTokens.contains(components[3]) else {
logger.warning("Invalid path or secret: \(path)")
await sendHTTPResponse(connection, status: "403 Forbidden", body: #"{"error":"invalid path"}"#)
return
Expand All @@ -397,7 +408,7 @@ actor PermissionServer {
id: hookRequest.toolUseId,
toolName: hookRequest.toolName,
toolInput: hookRequest.toolInput,
runToken: runToken,
runToken: components[3],
streamPermissionMode: streamMode,
sessionId: hookRequest.sessionId
)
Expand Down
2 changes: 2 additions & 0 deletions RxCodeMobile/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Scan a QR code shown by RxCode on your Mac to pair this device.</string>
<key>NSLocalNetworkUsageDescription</key>
Expand Down