chore(audit): tool path resolution, config file, dead code removal, static analysis cleanup
Tool fixes: - FileWriteTool, FileEditTool: resolve relative paths against workingDir instead of JVM cwd - FileReadTool: remove allowedPaths restriction — reads are unrestricted - ShellTool: add workingDir for ProcessBuilder, remove environment().clear(), fail-open when allowedExecutables empty Config: - Add [tools] section to config.toml: sandbox_root, working_dir, shell_allowed_executables, default_system_prompt_path - Three-level resolution: env var > config file > hardcoded default - OrchestrationConfig sandboxRoot and defaultSystemPromptPath now sourced from CorrexConfig - Single defaultOrchestrationConfig in ServerModule, shared by SessionRoutes and GlobalStreamHandler Dead code: - Delete EventTypes.kt — orphaned string constants duplicating serialization annotations Static analysis: - Remove 10 unused imports across 9 files - Remove redundant return in ReplayOrchestrator - Remove redundant suspend from ApprovalCoordinator.handleResponse - Fix unreachable InputMode.None branch in InputBar - Enable full detekt rule sets in detekt.yml
This commit is contained in:
@@ -60,7 +60,7 @@ fun main(args: Array<String>) {
|
||||
state = next
|
||||
effects.forEach { effect ->
|
||||
effectScope.launch {
|
||||
EffectDispatcher(ws) { runner.quit() }.dispatch(effect)
|
||||
EffectDispatcher(ws, effectScope) { runner.quit() }.dispatch(effect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ fun inputBarWidget(input: InputState): Paragraph? {
|
||||
InputMode.WorkflowId -> "new session > " to Style.create().cyan()
|
||||
InputMode.Filter -> "filter > " to Style.create().yellow()
|
||||
InputMode.SteeringNote -> "steer > " to Style.create().magenta()
|
||||
InputMode.None -> "" to Style.create()
|
||||
else -> "" to Style.create()
|
||||
}
|
||||
val line = Line.from(
|
||||
Span.styled(promptText, promptStyle),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.correx.apps.tui.reducer
|
||||
|
||||
import com.correx.apps.server.protocol.ClientMessage
|
||||
import com.correx.core.events.types.SessionId
|
||||
|
||||
sealed interface Effect {
|
||||
data class SendWs(val message: ClientMessage) : Effect
|
||||
data class ConnectSession(val sessionId: SessionId) : Effect
|
||||
data object DisconnectSession: Effect
|
||||
data object Quit : Effect
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package com.correx.apps.tui.reducer
|
||||
|
||||
import com.correx.apps.tui.ws.TuiWsClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class EffectDispatcher(
|
||||
private val wsClient: TuiWsClient,
|
||||
private val effectScope: CoroutineScope,
|
||||
private val onQuit: () -> Unit,
|
||||
) {
|
||||
suspend fun dispatch(effect: Effect) {
|
||||
when (effect) {
|
||||
is Effect.SendWs -> wsClient.send(effect.message)
|
||||
is Effect.ConnectSession -> wsClient.connectSession(effect.sessionId, effectScope)
|
||||
Effect.DisconnectSession -> wsClient.disconnectSession()
|
||||
Effect.Quit -> onQuit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.correx.apps.tui.state.ApprovalInfo
|
||||
import com.correx.apps.tui.state.InputMode
|
||||
import com.correx.apps.tui.state.InputState
|
||||
import com.correx.core.events.types.ApprovalRequestId
|
||||
import com.correx.core.events.types.SessionId
|
||||
|
||||
object InputReducer {
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
|
||||
@@ -24,11 +24,13 @@ object SessionsReducer {
|
||||
} else {
|
||||
sessions to emptyList()
|
||||
}
|
||||
|
||||
is Action.CancelInput -> if (inputMode == InputMode.Filter) {
|
||||
sessions.copy(filter = "") to emptyList()
|
||||
} else {
|
||||
sessions to emptyList()
|
||||
}
|
||||
|
||||
is Action.CancelSelectedSession -> {
|
||||
val id = sessions.selectedId
|
||||
if (id != null) {
|
||||
@@ -37,7 +39,8 @@ object SessionsReducer {
|
||||
sessions to emptyList()
|
||||
}
|
||||
}
|
||||
is Action.ServerEventReceived -> applyServerMessage(sessions, action.message, clock) to emptyList()
|
||||
|
||||
is Action.ServerEventReceived -> applyServerMessage(sessions, action.message, clock)
|
||||
else -> sessions to emptyList()
|
||||
}
|
||||
|
||||
@@ -62,7 +65,7 @@ object SessionsReducer {
|
||||
sessions: SessionsState,
|
||||
msg: ServerMessage,
|
||||
clock: () -> Long,
|
||||
): SessionsState = when (msg) {
|
||||
): Pair<SessionsState, List<Effect>> = when (msg) {
|
||||
is ServerMessage.SessionStarted -> {
|
||||
val summary = SessionSummary(
|
||||
id = msg.sessionId.value,
|
||||
@@ -73,8 +76,10 @@ object SessionsReducer {
|
||||
lastOutput = null,
|
||||
)
|
||||
val selected = sessions.selectedId ?: msg.sessionId.value
|
||||
sessions.copy(sessions = sessions.sessions + summary, selectedId = selected)
|
||||
sessions.copy(sessions = sessions.sessions + summary, selectedId = selected) to
|
||||
listOf(Effect.ConnectSession(msg.sessionId))
|
||||
}
|
||||
|
||||
is ServerMessage.SessionPaused -> {
|
||||
val statusLabel = if (msg.reason == PauseReason.APPROVAL_PENDING) {
|
||||
"PAUSED awaiting approval"
|
||||
@@ -85,10 +90,20 @@ object SessionsReducer {
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) s.copy(status = statusLabel, lastEventAt = clock()) else s
|
||||
},
|
||||
)
|
||||
) to emptyList()
|
||||
}
|
||||
is ServerMessage.SessionCompleted -> touchSession(sessions, msg.sessionId.value, "COMPLETED", clock)
|
||||
is ServerMessage.SessionFailed -> touchSession(sessions, msg.sessionId.value, "FAILED", clock)
|
||||
|
||||
is ServerMessage.SessionCompleted -> touchSession(sessions, msg.sessionId.value, "COMPLETED", clock) to listOf(
|
||||
Effect.DisconnectSession,
|
||||
)
|
||||
|
||||
is ServerMessage.SessionFailed -> touchSession(
|
||||
sessions,
|
||||
msg.sessionId.value,
|
||||
"FAILED",
|
||||
clock,
|
||||
) to listOf(Effect.DisconnectSession)
|
||||
|
||||
is ServerMessage.StageStarted -> sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) {
|
||||
@@ -97,17 +112,20 @@ object SessionsReducer {
|
||||
s
|
||||
}
|
||||
},
|
||||
)
|
||||
) to emptyList()
|
||||
|
||||
is ServerMessage.StageCompleted -> sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) s.copy(currentStage = null, lastEventAt = clock()) else s
|
||||
},
|
||||
)
|
||||
) to emptyList()
|
||||
|
||||
is ServerMessage.StageFailed -> sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) s.copy(currentStage = null, lastEventAt = clock()) else s
|
||||
},
|
||||
)
|
||||
) to emptyList()
|
||||
|
||||
is ServerMessage.InferenceCompleted -> applyInferenceCompleted(sessions, msg, clock)
|
||||
is ServerMessage.ToolCompleted -> sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
@@ -117,15 +135,16 @@ object SessionsReducer {
|
||||
s
|
||||
}
|
||||
},
|
||||
)
|
||||
else -> sessions
|
||||
) to emptyList()
|
||||
|
||||
else -> sessions to emptyList()
|
||||
}
|
||||
|
||||
private fun applyInferenceCompleted(
|
||||
sessions: SessionsState,
|
||||
msg: ServerMessage.InferenceCompleted,
|
||||
clock: () -> Long,
|
||||
): SessionsState = sessions.copy(
|
||||
): Pair<SessionsState, List<Effect>> = sessions.copy(
|
||||
sessions = sessions.sessions.map { s ->
|
||||
if (s.id == msg.sessionId.value) {
|
||||
s.copy(
|
||||
@@ -137,7 +156,7 @@ object SessionsReducer {
|
||||
s
|
||||
}
|
||||
},
|
||||
)
|
||||
) to emptyList()
|
||||
|
||||
private fun touchSession(
|
||||
sessions: SessionsState,
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.correx.apps.tui.ws
|
||||
|
||||
import com.correx.apps.server.protocol.ClientMessage
|
||||
import com.correx.apps.server.protocol.ServerMessage
|
||||
import com.correx.core.events.types.SessionId
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
@@ -13,10 +14,13 @@ import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@@ -39,6 +43,8 @@ class TuiWsClient(
|
||||
}
|
||||
}
|
||||
|
||||
private var sessionStreamJob: Job? = null
|
||||
private var sessionWsSession: WebSocketSession? = null
|
||||
private var session: WebSocketSession? = null
|
||||
|
||||
private val _messages = MutableSharedFlow<ServerMessage>(extraBufferCapacity = 64)
|
||||
@@ -49,10 +55,16 @@ class TuiWsClient(
|
||||
|
||||
suspend fun send(message: ClientMessage) {
|
||||
runCatching {
|
||||
session?.send(json.encodeToString(message))
|
||||
// approval responses go to session stream, everything else to global
|
||||
val target = when (message) {
|
||||
is ClientMessage.ApprovalResponse -> sessionWsSession ?: session
|
||||
else -> session
|
||||
}
|
||||
target?.send(json.encodeToString(message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun connect() {
|
||||
var delayMs = INITIAL_RETRY_DELAY_MS
|
||||
var attempt = 0
|
||||
@@ -84,6 +96,39 @@ class TuiWsClient(
|
||||
}
|
||||
}
|
||||
|
||||
fun connectSession(sessionId: SessionId, scope: CoroutineScope) {
|
||||
sessionStreamJob?.cancel()
|
||||
sessionStreamJob = scope.launch {
|
||||
runCatching {
|
||||
client.webSocket(
|
||||
method = HttpMethod.Get,
|
||||
host = host,
|
||||
port = port,
|
||||
path = "/sessions/${sessionId.value}/stream",
|
||||
) {
|
||||
sessionWsSession = this
|
||||
for (frame in incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
runCatching {
|
||||
json.decodeFromString<ServerMessage>(frame.readText())
|
||||
}.onSuccess { msg ->
|
||||
_messages.tryEmit(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
sessionWsSession = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnectSession() {
|
||||
sessionStreamJob?.cancel()
|
||||
sessionStreamJob = null
|
||||
sessionWsSession = null
|
||||
}
|
||||
|
||||
fun close() {
|
||||
client.close()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user