From d405f9d2c18ea323c561641faa3f36151f1147c3 Mon Sep 17 00:00:00 2001 From: kami Date: Tue, 2 Jun 2026 19:56:49 +0400 Subject: [PATCH] feat(workspace): WorkspaceContext + SessionWorkspaceBoundEvent foundation Add a WorkspaceContext value type and a nullable OrchestrationConfig.workspace, plus SessionWorkspaceBoundEvent registered in eventModule. Purely additive; no behavior change when workspace is null. (Axis 2 Phase A, tasks 1-2.) --- .../core/events/events/SessionEvents.kt | 8 ++++ .../events/serialization/Serialization.kt | 2 + ...ionWorkspaceBoundEventSerializationTest.kt | 38 +++++++++++++++++++ .../orchestration/OrchestrationConfig.kt | 1 + .../kernel/orchestration/WorkspaceContext.kt | 10 +++++ 5 files changed, 59 insertions(+) create mode 100644 core/events/src/test/kotlin/com/correx/core/events/serialization/SessionWorkspaceBoundEventSerializationTest.kt create mode 100644 core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/WorkspaceContext.kt diff --git a/core/events/src/main/kotlin/com/correx/core/events/events/SessionEvents.kt b/core/events/src/main/kotlin/com/correx/core/events/events/SessionEvents.kt index 6de620c8..817525fd 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/events/SessionEvents.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/events/SessionEvents.kt @@ -9,3 +9,11 @@ import kotlinx.serialization.Serializable data class ChatSessionStartedEvent( val sessionId: SessionId, ) : EventPayload + +@Serializable +@SerialName("SessionWorkspaceBound") +data class SessionWorkspaceBoundEvent( + val sessionId: SessionId, + val workspaceRoot: String, + val allowedPaths: List, +) : EventPayload diff --git a/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt b/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt index cd98fa5e..3fa3efb0 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/serialization/Serialization.kt @@ -9,6 +9,7 @@ import com.correx.core.events.events.ArtifactValidatedEvent import com.correx.core.events.events.ArtifactValidatingEvent import com.correx.core.events.events.ChatSessionStartedEvent import com.correx.core.events.events.ChatTurnEvent +import com.correx.core.events.events.SessionWorkspaceBoundEvent import com.correx.core.events.events.ContextTruncatedEvent import com.correx.core.events.events.EventPayload import com.correx.core.events.events.FileWrittenEvent @@ -79,6 +80,7 @@ val eventModule = SerializersModule { subclass(RiskAssessedEvent::class) subclass(ChatSessionStartedEvent::class) subclass(ChatTurnEvent::class) + subclass(SessionWorkspaceBoundEvent::class) subclass(L3MemoryRetrievedEvent::class) subclass(ContextTruncatedEvent::class) } diff --git a/core/events/src/test/kotlin/com/correx/core/events/serialization/SessionWorkspaceBoundEventSerializationTest.kt b/core/events/src/test/kotlin/com/correx/core/events/serialization/SessionWorkspaceBoundEventSerializationTest.kt new file mode 100644 index 00000000..9e3f5305 --- /dev/null +++ b/core/events/src/test/kotlin/com/correx/core/events/serialization/SessionWorkspaceBoundEventSerializationTest.kt @@ -0,0 +1,38 @@ +package com.correx.core.events.serialization + +import com.correx.core.events.events.EventPayload +import com.correx.core.events.events.SessionWorkspaceBoundEvent +import com.correx.core.events.types.SessionId +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SessionWorkspaceBoundEventSerializationTest { + + private val sample = SessionWorkspaceBoundEvent( + sessionId = SessionId("sess-1"), + workspaceRoot = "/home/user/project", + allowedPaths = listOf("/home/user/project", "/home/user/project/src"), + ) + + @Test + fun `round-trips as polymorphic EventPayload`() { + val encoded = eventJson.encodeToString(EventPayload.serializer(), sample) + assertTrue(encoded.contains("\"type\":\"SessionWorkspaceBound\""), "SerialName must be present: $encoded") + val decoded = eventJson.decodeFromString(EventPayload.serializer(), encoded) + assertEquals(sample, decoded) + } + + @Test + fun `decodes hand-written SessionWorkspaceBound JSON`() { + val json = """ + {"type":"SessionWorkspaceBound","sessionId":"s-42","workspaceRoot":"/ws","allowedPaths":["/ws","/ws/src"]} + """.trimIndent() + val decoded = eventJson.decodeFromString(EventPayload.serializer(), json) + assertTrue(decoded is SessionWorkspaceBoundEvent) + decoded as SessionWorkspaceBoundEvent + assertEquals("s-42", decoded.sessionId.value) + assertEquals("/ws", decoded.workspaceRoot) + assertEquals(listOf("/ws", "/ws/src"), decoded.allowedPaths) + } +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt index 0fe83203..53c342a9 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/OrchestrationConfig.kt @@ -10,4 +10,5 @@ data class OrchestrationConfig( val stageTimeoutMs: Long = 60_000L, val sandboxRoot: Path = Path.of(System.getProperty("java.io.tmpdir"), "correx-sandbox"), val defaultSystemPromptPath: String? = null, + val workspace: WorkspaceContext? = null, ) diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/WorkspaceContext.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/WorkspaceContext.kt new file mode 100644 index 00000000..e75f040a --- /dev/null +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/WorkspaceContext.kt @@ -0,0 +1,10 @@ +package com.correx.core.kernel.orchestration + +import java.nio.file.Path + +data class WorkspaceContext( + val workspaceRoot: Path, + val workingDir: Path, + val allowedPaths: Set, + val privilegedLocations: List = emptyList(), +)