harden: enforce path-safe subagent IDs at ExtractSpawnedAgentIDs · Entire

harden: enforce path-safe subagent IDs at ExtractSpawnedAgentIDs

b1338e1→main·

Soph·1mo ago·2 files·+12 added/-4 removed

CalculateTotalTokenUsage and ExtractAllModifiedFiles build agent-.jsonl from subagent IDs and read that file, with no local validation of the ID. Today the IDs are path-safe only because their sole source, extractAgentIDFromText, happens to accept just [a-zA-Z0-9]. That makes the path-safety of a file read depend on the incidental character set of a parser two calls away — a fragile coupling: relaxing extractAgentIDFromText (e.g. to accept hyphenated UUIDs) would silently open a traversal-read of arbitrary agent-*.jsonl-shaped paths.

Enforce the invariant at the choke point: ExtractSpawnedAgentIDs now drops any ID that fails validation.ValidateAgentID, so every downstream agent-.jsonl consumer (claudecode + factoryaidroid, token usage and modified-files) is path-safe by construction.

Not a live vulnerability — the current parser already constrains the ID, so the guard is a no-op for today's inputs and adds no behavior change. No test is added: the guarded branch is unreachable through the real extractor (it can't emit a non-path-safe ID), so a test would only exercise dead input.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

c56a01dc268eView transcript

Changes

2

8 unmodified lines

9
10
11
12
13
14
15
239 unmodified lines

255
256
257
257
258
258
259
260
261
262
263
264
265

8 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/agent"
    "github.com/entireio/cli/cmd/entire/cli/transcript"
    "github.com/entireio/cli/cmd/entire/cli/validation"
// TranscriptLine is an alias to the shared transcript.Line type.
239 unmodified lines

}
    }

// Look for agentId in the text
        if agentID := extractAgentIDFromText(textContent); agentID != "" {
        // Look for agentId in the text. Drop any ID that isn't path-safe:
        // callers build agent-<id>.jsonl from it and read that file, so this
        // is the choke point that keeps the path inside subagentsDir,
        // independent of extractAgentIDFromText's character handling.
        if agentID := extractAgentIDFromText(textContent); agentID != "" && validation.ValidateAgentID(agentID) == nil {
            agentIDs[agentID] = block.ToolUseID
        }
    }

Mcmd/entire/cli/agent/claudecode/transcript.go+6/-2

13 unmodified lines

14
15
16
17
18
19
20
250 unmodified lines

271
272
273
273
274
274
275
276
277
278
279
280
281

13 unmodified lines

"github.com/entireio/cli/cmd/entire/cli/agent"
    "github.com/entireio/cli/cmd/entire/cli/transcript"
    "github.com/entireio/cli/cmd/entire/cli/validation"
// TranscriptLine is an alias to the shared transcript.Line type.
250 unmodified lines

}
    }

Mcmd/entire/cli/agent/factoryaidroid/transcript.go+6/-2