feat(toolintent): read-before-write gate (anti-hallucination)

An LLM that edits or overwrites a file it never read is acting on hallucinated
contents. This plane-2 ToolCallRule blocks it: dispatching on FILE_WRITE (covers
file_write and file_edit), a write to a path that exists on disk but was not
file_read earlier this session is BLOCKED. A brand-new file passes — you can't
read what doesn't exist, and creating is legitimate.

- ReadFilesProjection folds the session's file_read events (request + completion,
  so a failed read doesn't count) into the set of paths read, mirroring
  EgressAllowlistProjection; threaded into ToolCallAssessmentInput.sessionReadPaths
  via SessionOrchestrator.resolveSessionReadPaths.
- Read paths and the write target are both resolved through the probe (toRealPath),
  so spelling differences and symlinks can't dodge the gate.
- The orchestrator already feeds a block's rationale back as a tool result, so the
  agent reads the file and retries; the rejected-event reason now carries that
  rationale too (specific audit trail instead of a generic string).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 18:13:06 +00:00
parent 5d48c26ec8
commit 693e38ffac
7 changed files with 313 additions and 1 deletions
@@ -61,6 +61,7 @@ import com.correx.core.toolintent.rules.ExecInterpreterRule
import com.correx.core.toolintent.rules.ManifestContainmentRule
import com.correx.core.toolintent.rules.NetworkHostRule
import com.correx.core.toolintent.rules.PathContainmentRule
import com.correx.core.toolintent.rules.ReadBeforeWriteRule
import com.correx.apps.server.freestyle.FreestyleDriver
import com.correx.apps.server.inference.summarizeWithInference
import com.correx.core.kernel.orchestration.JournalCompactionService
@@ -261,6 +262,7 @@ fun main() {
val toolCallAssessor = ToolCallAssessor(
rules = listOf(
PathContainmentRule(),
ReadBeforeWriteRule(),
ManifestContainmentRule(),
ExecInterpreterRule(toolsConfig.interpreterExecutables.toSet()),
NetworkHostRule(