Skip to content

Scan cost double-counts unrelated sessions: ScanCostTracker session-attribution is weaker than readScanLogs #581

Description

@Drewwb

Summary

ScanCostTracker (sdk/typescript/src/cost.ts) and readScanLogs (sdk/typescript/src/scan-logs.ts) both decide "does this Codex session belong to this scan?" using the deep-worker output directory layout but they use different checks, and the cost one is weaker. As a result ScanCostTracker attributes token usage from sessions that readScanLogs deliberately excludes. Because cost also gates --max-cost, this can both misreport cost and abort a scan early against its budget.

Environment

  • @openai/codex-security 0.1.16 (repo main @ 97d2c83)
  • OS: platform-independent (path logic); one trigger is Windows-specific
  • Area: sdk/typescript/src/cost.ts, sdk/typescript/src/scan-logs.ts

Details

readScanLogsbelongsToScan requires the session working directory to be exactly <scanDir>/artifacts/deep_discovery/workers/<id>/output:

const directory = relative(workers, session.workingDirectory);
const components = directory.split(sep);
if (
  !isAbsolute(directory) &&
  components.length === 2 &&
  components[0] !== ".." &&
  relative(join(workers, components[0]!, "output"), session.workingDirectory) === ""
) { return true; }

ScanCostTracker.#readSessions only checks that the relative path splits into two components whose second is "output", dropping the !isAbsolute, components[0] !== "..", and exact-path guards:

const workerDirectory = relative(
  join(artifactsDirectory, "deep_discovery", "workers"),
  session.workingDirectory,
).split(sep);
if (
  session.workingDirectory === artifactsDirectory ||
  (workerDirectory.length === 2 && workerDirectory[1] === "output")
) { included.add(session.threadId); }

Two working directories that belongsToScan rejects but #readSessions accepts:

  1. Sibling of workers/ (any OS): <scanDir>/artifacts/deep_discovery/output
    relative(workers, cwd) = ../output["..","output"] → length 2, [1] === "output".
    belongsToScan rejects it via components[0] !== "..".
  2. Windows cross-drive: D:\output while the scan directory is on C:
    relative() returns the absolute D:\output["D:","output"] → length 2, [1] === "output".
    belongsToScan rejects it via !isAbsolute(directory).

Reproduction

Running the two predicates' verbatim source expressions on identical inputs (node, path.win32):

scanDir = C:\srv\scans\run1

wd = C:\srv\scans\run1\artifacts\deep_discovery\workers\w0\output   cost=include  logs=include   (agree)
wd = C:\srv\scans\run1\artifacts\deep_discovery\output              cost=include  logs=EXCLUDE   (diverge)
wd = D:\output                                                      cost=include  logs=EXCLUDE   (diverge)

End-to-end, a session running in <scanDir>/artifacts/deep_discovery/output (started after the scan) with, say, 1,000,000 input / 1,000,000 output tokens is folded into the scan's reported usage, even though its events are not part of the scan's log view.

Expected vs. observed

  • Expected: cost attribution matches log attribution, only the scan thread, real workers/<id>/output sessions, the artifacts dir, and their child threads are counted.
  • Observed: ScanCostTracker additionally counts sessions in sibling directories under deep_discovery/, and (on Windows) any X:\output directory on a different drive, inflating cost and token totals and potentially tripping --max-cost early.

Suggested fix

Have #readSessions use the same guarded, exact-path check as belongsToScan (add isAbsolute import; require !isAbsolute, components[0] !== "..", and the relative(join(workers, components[0]!, "output"), cwd) === "" re-check). With that change the two predicates agree on every input, real worker/artifacts directories are still counted, and the bystander directories are excluded. Happy to open a focused PR if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions