feat(artifacts): config-driven custom artifact kinds with schema validation

Users declare artifact kinds in [[artifacts]] (id + schema_path to a JSON-schema file + llm_emitted); ConfigArtifactKind registers them at startup via createWorkflowLoader(extraKinds). New JsonSchemaValidator validates any non-built-in kind generically against its declared deriveJsonSchema(), so an LLM-emitted custom kind is checked against its shape, never trusted. Removed the dead payloadSerializer from the ArtifactKind contract. Schema source is path-to-file (not inline TOML — the hand-rolled parser can't nest).
This commit is contained in:
2026-06-02 13:58:23 +04:00
parent b976a5c92a
commit d1c6774d05
14 changed files with 315 additions and 30 deletions
@@ -1,10 +1,7 @@
package com.correx.core.artifacts.kind
import kotlinx.serialization.KSerializer
interface ArtifactKind {
val id: String
val payloadSerializer: KSerializer<*>
fun deriveJsonSchema(): JsonSchema
val llmEmitted: Boolean
}
@@ -0,0 +1,16 @@
package com.correx.core.artifacts.kind
/**
* A user-declared artifact kind whose schema comes from configuration rather than
* a compile-time Kotlin type. The declared [schema] is authoritative: content is
* validated against it generically (see the artifact-payload validator), so an
* [llmEmitted] config kind is checked against its declared shape, never a
* hardcoded type check.
*/
data class ConfigArtifactKind(
override val id: String,
private val schema: JsonSchema,
override val llmEmitted: Boolean = false,
) : ArtifactKind {
override fun deriveJsonSchema(): JsonSchema = schema
}
@@ -1,10 +1,7 @@
package com.correx.core.artifacts.kind
import kotlinx.serialization.KSerializer
object FileWrittenKind : ArtifactKind {
override val id: String = "file_written"
override val payloadSerializer: KSerializer<*> = FileWrittenPayload.serializer()
override val llmEmitted: Boolean = false
override fun deriveJsonSchema(): JsonSchema = JsonSchema(
@@ -1,10 +1,7 @@
package com.correx.core.artifacts.kind
import kotlinx.serialization.KSerializer
object ProcessResultKind : ArtifactKind {
override val id: String = "process_result"
override val payloadSerializer: KSerializer<*> = ProcessResultArtifact.serializer()
override val llmEmitted: Boolean = false
override fun deriveJsonSchema(): JsonSchema = JsonSchema(
@@ -29,6 +29,21 @@ class ArtifactKindRegistryTest {
assertEquals(false, registry.get("process_result")!!.llmEmitted)
}
@Test
fun `config-declared kind registers and exposes its declared schema`() {
val schema = JsonSchema(
type = "object",
properties = mapOf("x" to JsonSchemaProperty(type = "string")),
required = listOf("x"),
)
registry.register(ConfigArtifactKind(id = "custom", schema = schema, llmEmitted = true))
val kind = registry.get("custom")
assertNotNull(kind)
assertEquals(true, kind!!.llmEmitted)
assertEquals(schema, kind.deriveJsonSchema())
}
@Test
fun `FileWrittenKind schema has required fields`() {
val schema = FileWrittenKind.deriveJsonSchema()