Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ These are the numbers from commands, not from a slide.

That block is what `agentmetry benchmark` printed from the PyPI 0.9.4 package and from this tree. "Rules covered 13" counts rule ids named in corpus expectations. Twelve of those are published rules. The thirteenth is the experimental rule. Two published rules have no corpus case: `host-subagent-swarm-burst` and `off-hours-activity`. They are named in `apps/orchestrator/tools/generate_sigma_pack.py` so the Sigma pack still has them.

2258 tests collected (`pytest --collect-only -q` from `apps/orchestrator`). The
2269 tests collected (`pytest --collect-only -q` from `apps/orchestrator`). The
[0.10.6 release PR's CI run](https://github.com/blitzcrieg1/agentmetry/actions/runs/42082231888)
on Python 3.12 reported 2089 passed and 19 skipped on Linux and macOS,
2102 passed and 6 skipped on Windows, and 82.13% line coverage on Linux
Expand Down
34 changes: 28 additions & 6 deletions apps/orchestrator/agentmetry/core/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
#: The first line of a Codex session file also holds its base instructions.
_SESSION_META_MAX_BYTES = 1024 * 1024
_WINDOWS_EXTENDED_PREFIX = chr(92) * 2 + "?" + chr(92)
_CLAUDE_AUTH_OVERRIDE_VARS = (
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
Expand Down Expand Up @@ -244,7 +245,30 @@ def _local_antigravity_account() -> dict[str, str]:
return {}


def _codex_session_account(transcript_path: Any) -> dict[str, str]:
def _codex_session_file(home: Path, transcript_path: Any, session_id: Any) -> Path | None:
"""Find the session file, from the path the hook named or else from its ID.

Windows tools can hand over the extended form of a path, which would not
match Codex's home. A session with no usable path is found by its ID, which
Codex puts in the file name.
"""
if isinstance(transcript_path, str) and transcript_path.endswith(".jsonl"):
text = transcript_path
if text.startswith(_WINDOWS_EXTENDED_PREFIX):
text = text[len(_WINDOWS_EXTENDED_PREFIX) :]
path = Path(text).resolve()
if path.is_relative_to(home) and path.is_file():
return path
session = _clean_id(session_id)
if not session or not session.replace("-", "").isalnum():
return None
# sessions/YYYY/MM/DD/rollout-<time>-<session id>.jsonl
for path in (home / "sessions").glob(f"*/*/*/rollout-*-{session}.jsonl"):
return path.resolve()
return None


def _codex_session_account(transcript_path: Any, session_id: Any = "") -> dict[str, str]:
"""Read Codex's account IDs from the first line of its session file.

Hook input names the file, but that format is not a stable interface and
Expand All @@ -253,13 +277,11 @@ def _codex_session_account(transcript_path: Any) -> dict[str, str]:
read past the first line, and `auth.json` is never touched: it holds
credentials, and a security sensor has no business opening it.
"""
if not isinstance(transcript_path, str) or not transcript_path.endswith(".jsonl"):
return {}
try:
configured = os.environ.get("CODEX_HOME", "").strip()
home = (Path(configured) if configured else Path.home() / ".codex").resolve()
path = Path(transcript_path).resolve()
if not path.is_relative_to(home) or not path.is_file():
path = _codex_session_file(home, transcript_path, session_id)
if path is None:
return {}
with path.open("rb") as handle:
line = handle.readline(_SESSION_META_MAX_BYTES + 1)
Expand Down Expand Up @@ -289,7 +311,7 @@ def account_for_hook(source_app: str, data: dict[str, Any]) -> dict[str, str]:
if source == "claude":
return _local_claude_account()
if source == "codex":
return _codex_session_account(data.get("transcript_path"))
return _codex_session_account(data.get("transcript_path"), data.get("session_id"))
if source == "antigravity":
return _local_antigravity_account()
return {}
Expand Down
32 changes: 32 additions & 0 deletions apps/orchestrator/tests/test_codex_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,38 @@ def test_the_ids_come_from_the_session_file(codex_home: Path):
}


def test_the_windows_extended_form_of_the_path_is_accepted(codex_home: Path):
path = _session(codex_home)
extended = chr(92) * 2 + "?" + chr(92) + str(path)
assert account_for_hook("codex", {"transcript_path": extended})["account_id"] == "workspace-1"


def test_a_session_with_no_path_is_found_by_its_id(codex_home: Path):
session = "01a1a857-aaaa-bbbb-cccc-000000000000"
_session(codex_home, name=f"rollout-2026-11-07T10-00-00-{session}.jsonl")
assert account_for_hook("codex", {"session_id": session})["account_id"] == "workspace-1"
assert account_for_hook("codex", {"transcript_path": None, "session_id": session})["user_id"] == "user-1"


def test_a_path_that_fails_falls_back_to_the_id(codex_home: Path, tmp_path: Path):
session = "01a1a857-aaaa-bbbb-cccc-000000000000"
_session(codex_home, name=f"rollout-2026-11-07T10-00-00-{session}.jsonl")
outside = _session(tmp_path / "elsewhere", {"creator_account_id": "not-this-one"})
block = account_for_hook("codex", {"transcript_path": str(outside), "session_id": session})
assert block["account_id"] == "workspace-1"


def test_another_session_id_does_not_match(codex_home: Path):
_session(codex_home, name="rollout-2026-11-07T10-00-00-01a1a857-aaaa.jsonl")
assert account_for_hook("codex", {"session_id": "01a1a857-bbbb"}) == {}


@pytest.mark.parametrize("session", ["*", "..", "a/b", "a*", "../../etc", "rollout", ""])
def test_a_crafted_session_id_cannot_widen_the_search(codex_home: Path, session: str):
_session(codex_home, name="rollout-2026-11-07T10-00-00-01a1a857-aaaa.jsonl")
assert account_for_hook("codex", {"session_id": session}) == {}


def test_a_file_outside_codex_home_is_not_opened(codex_home: Path, tmp_path: Path):
outside = _session(tmp_path / "elsewhere")
assert account_for_hook("codex", {"transcript_path": str(outside)}) == {}
Expand Down
3 changes: 2 additions & 1 deletion docs/agentmetry-event-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ federation, or routed-access environment state selects another credential.
`ANTHROPIC_BASE_URL=https://api.anthropic.com` is the one exception: its
official host keeps the fallback, while other, look-alike, or malformed URLs
skip it. Codex's hook input has no account field, but it names the session file,
whose first line carries `creator_account_id` and `creator_user_id`. Those arrive
whose first line carries `creator_account_id` and `creator_user_id`. If the path
is missing or unusable, the file is found by session ID in its name. Those arrive
as `account_id` and `user_id` with source `session_meta`. That format is not a
stable interface and can change between versions, and only a file under Codex's
home (`CODEX_HOME`, else `~/.codex`) is opened. `auth.json` is never read, because
Expand Down
Loading