feat(workspace): Axis 2 Phase B — client→server workspace handshake
Wires the workspace handshake end to end so a session's workspace is bound
from the client's cwd at connect time and is event-sourced for replay.
- Go TUI sends Hello{workingDir=os.Getwd()} as the first WS frame on connect
(protocol.go encoder + client.go connect path; golden test pins the wire
format against the Kotlin discriminator).
- Server adds ClientMessage.Hello, stashes the per-connection workingDir, and
on session start resolves it through WorkspaceResolver's trust pipeline,
emits SessionWorkspaceBoundEvent (invariant #9), and threads the resolved
workspace into OrchestrationConfig for the live run. A Hello after the first
StartSession is ignored (warn); a rejected path binds the resolver fallback.
- Replay derives the workspace from the recorded event: SessionState gains
boundWorkspace, DefaultSessionReducer fills it from SessionWorkspaceBoundEvent,
and ReplayOrchestrator uses it (Path.of only, no filesystem re-query —
invariant #8) with graceful fallback to config for pre-Phase-B logs.
Absent Hello / null resolver degrades to the prior config-workspace behavior.
This commit is contained in:
+19
-3
@@ -23,6 +23,7 @@ import com.correx.core.transitions.graph.WorkflowGraph
|
||||
import com.correx.core.transitions.resolution.TransitionDecision
|
||||
import com.correx.core.validation.model.ValidationContext
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.atomic.*
|
||||
@@ -65,10 +66,25 @@ class ReplayOrchestrator(
|
||||
graph: WorkflowGraph,
|
||||
config: OrchestrationConfig,
|
||||
): WorkflowResult {
|
||||
emitWorkflowStarted(sessionId, graph, config)
|
||||
|
||||
val session = repositories.sessionRepository.getSession(sessionId)
|
||||
val ctx = ReplayContext(graph, sessionId, graph.start, 0, config)
|
||||
|
||||
// Derive workspace from the recorded SessionWorkspaceBoundEvent (invariants #1, #8, #9).
|
||||
// The resolver ran at handshake time; replay reads its recorded decision — no live FS calls.
|
||||
val effectiveConfig = session.state.boundWorkspace
|
||||
?.let { bw ->
|
||||
config.copy(
|
||||
workspace = WorkspaceContext(
|
||||
workspaceRoot = Path.of(bw.workspaceRoot),
|
||||
workingDir = Path.of(bw.workspaceRoot),
|
||||
allowedPaths = bw.allowedPaths.map { Path.of(it) }.toSet(),
|
||||
)
|
||||
)
|
||||
}
|
||||
?: config
|
||||
|
||||
emitWorkflowStarted(sessionId, graph, effectiveConfig)
|
||||
|
||||
val ctx = ReplayContext(graph, sessionId, graph.start, 0, effectiveConfig)
|
||||
|
||||
// Execute the start stage before entering the step loop
|
||||
return when (val result = enterStage(ctx, graph.start, session)) {
|
||||
|
||||
Reference in New Issue
Block a user