πŸ“ tools/FileWriteTool/ Β· 03_safety___state_validation.md

Chapter 3: Safety & State Validation

πŸ“„ tools/FileWriteTool/03_safety___state_validation.md

Chapter 3: Safety & State Validation

Welcome to Chapter 3! In the previous chapter, LLM Prompt Strategy, we taught the AI the "rules of the road" via text prompts. We politely asked it to read files before writing them.

But as any parent knows, asking politely doesn't always work. Sometimes mistakes happen.

This chapter is about Safety & State Validation. If Chapter 1 was the "Hand" and Chapter 2 was the "Instruction Manual," this chapter is the Reflex System. It is the hard-coded logic that automatically stops the hand from touching a hot stove.

The Motivation: The "Stale" Write Problem

The Central Use Case: Imagine this scenario:

  1. You ask the AI to refactor app.js.
  2. The AI reads app.js.
  3. While the AI is thinking, you realize a typo in app.js and quickly fix it yourself.
  4. The AI finishes thinking and writes its new version.

The Problem: The AI's version was based on the old file. If it writes now, it will delete the typo fix you just made. This is called a "Stale Write."

The Solution: We need a mechanism that acts like a bank teller. Before finalizing a transaction, the teller checks: "Has the account balance changed since I last looked?"

Key Concept 1: The Guard Phase (validateInput)

In Chapter 1, we saw the call() method. But there is another method that runs before call(): validateInput.

Think of validateInput as a nightclub bouncer. It checks your ID before you are allowed inside to dance (execute code). If the bouncer says "No," the call() method is never touched.

// Conceptual Flow
if (await validateInput(input)) {
  return await call(input) // Write to disk
} else {
  return Error("Validation Failed") // Stop!
}

Key Concept 2: Permissions & Secrets

The first job of the bouncer is checking if the AI is allowed to be here.

  1. Secret Protection: We don't want the AI writing passwords or API keys into shared team memory files.
  2. Access Control: Is the AI trying to write to a system folder or a directory the user has explicitly blocked?
// Inside validateInput
const fullFilePath = expandPath(file_path)

// 1. Check for secrets
if (checkTeamMemSecrets(fullFilePath, content)) {
  return { result: false, message: "Contains secrets!" }
}

// 2. Check user permissions
if (matchingRuleForInput(fullFilePath, ..., 'deny')) {
  return { result: false, message: "Permission denied." }
}

Explanation:

Key Concept 3: Staleness Checks (Time Travel Prevention)

This is the most critical safety feature. We need to ensure the file hasn't changed since the AI last read it.

We track this using Timestamps.

If the Last Write Time is newer than the Last Read Time, the AI is looking at a ghost. We must reject the write.

// Inside validateInput
const lastRead = readFileState.get(fullFilePath)

// If the AI never read the file, stop it!
if (!lastRead) {
    return { result: false, message: 'Read it first!' }
}

// If file on disk is newer than our memory of it
if (fileMtimeMs > lastRead.timestamp) {
    return { result: false, message: 'File modified since read!' }
}

Internal Implementation: The Safety Sequence

Let's visualize the flow of data when the AI tries to write a file.

sequenceDiagram participant AI participant Tool as FileWriteTool participant State as Memory State participant FS as File System AI->>Tool: Request: Write "X" to "file.txt" Note over Tool: Phase 1: validateInput() Tool->>FS: What is mtime (Last Modified) of "file.txt"? FS-->>Tool: 10:05 AM Tool->>State: When did we last read "file.txt"? State-->>Tool: 10:00 AM Note over Tool: 10:05 > 10:00<br/>File changed! REJECT. Tool-->>AI: Error: File is stale. Read again.

Deep Dive: Handling the "Race Condition"

Even with validateInput, there is a tiny risk. What if the user saves the file in the split second after validateInput passes but before call writes?

To solve this, we perform a "Double Check" inside the call method, right before the write happens. This makes the operation Atomic.

Step 1: The Initial Check (in validateInput) This catches 99% of errors and gives the AI a helpful error message without attempting to write.

// src/tools/FileWriteTool.ts

// Get the timestamp from our internal state
const readTimestamp = toolUseContext.readFileState.get(fullFilePath)

// Compare against the actual file system
if (lastWriteTime > readTimestamp.timestamp) {
  return {
    result: false,
    message: 'File has been modified since read...',
    errorCode: 3,
  }
}

Step 2: The Atomic Guard (in call) Just before we commit to disk, we check one last time.

// src/tools/FileWriteTool.ts -> inside call()

// Read metadata one last time
const meta = readFileSyncWithMetadata(fullFilePath)

// If content on disk is different from what we expect
if (meta.content !== lastRead.content) {
  // ABORT!
  throw new Error(FILE_UNEXPECTEDLY_MODIFIED_ERROR)
}

// If we survive this line, it is safe to write
writeTextContent(fullFilePath, content, enc, 'LF')

Explanation:

Why this matters for Beginners

When building tools for AI, it is easy to trust the AI too much. You might think, "I told it to read the file first, so it will."

But in software engineering, Trust but Verify is the golden rule.

  1. Trust: The Prompt Strategy (Chapter 2).
  2. Verify: The State Validation (Chapter 3).

By layering these two approaches, we create a tool that is robust, safe, and trustworthy enough to run on a user's local machine.

Conclusion

We have now secured our tool.

  1. We check Permissions (Bouncer).
  2. We check Staleness (Bank Teller).
  3. We double-check Atomicity (Race Conditions).

Now that the write operation is safe and complete, the job isn't quite done. Writing a file impacts the rest of the worldβ€”IDEs need to refresh, linters need to run, and git needs to track changes.

Next Chapter: Ecosystem Integration (Side Effects)


Generated by Code IQ