Skip to content

Session transcripts

AAK-AGENT-COMPOSE-002 looks for instruction splicing: one intent fragmented across several individually-compliant tool calls in a single session, where the concatenation of their arguments reconstructs something a single call would have been denied for. That is the GhostSplice / cross-channel trust-fragmentation shape disclosed by the ASSET Research Group.

The rule needs an ordered list of tool calls. On its own it only discovers *.session.json files and JSON under .aak/sessions/ — the file you would write by hand, and not what any agent framework emits. --sessions closes that gap: it reads the transcripts frameworks actually write, normalises them into the same ordered list, and runs the unchanged rule over them.

# a directory of transcripts
agent-audit-kit scan . --sessions ./traces/

# a single transcript
agent-audit-kit scan . --sessions ./traces/run-412.json

# see which format each file was read as
agent-audit-kit scan . --sessions ./traces/ --verbose

Findings cite the transcript you passed, not an internal staging copy, so a report points at the file you can actually open.

Supported formats

Format is detected from the file's contents, not its name. A file that matches nothing is skipped rather than failing the scan.

Format Detected by Calls read from
openai-agents a spans / data / items list whose entries carry span_data spans with span_data.type == "function", using name and the JSON-string input
langgraph channel_values.messages, values.messages, or state.messages each message's tool_calls ({"name", "args"}), plus the legacy additional_kwargs.tool_calls OpenAI-function shape
jsonl one JSON object per line tool (or name / tool_name) and args (or arguments / input)
aak {"calls": [...]} or a bare list tool and arguments

OpenAI Agents SDK run traces

Exported run traces, {"spans": [...]} — a bare list of spans and the {"data": [...]} export envelope both work. Only function spans become calls; agent, generation, and handoff spans are ignored.

Spans are ordered by started_at when every span has one, otherwise file order is kept. Ordering matters: the rule's premise is that consecutive same-tool calls reassemble, so an out-of-order trace would hide a splice.

{
  "spans": [
    {"started_at": "2026-08-13T09:14:03.010Z",
     "span_data": {"type": "function", "name": "read_file",
                   "input": "{\"path\": \"/home/dev/\"}"}},
    {"started_at": "2026-08-13T09:14:03.500Z",
     "span_data": {"type": "function", "name": "read_file",
                   "input": "{\"path\": \".ssh/\"}"}}
  ]
}

An input that is not JSON is preserved under arg rather than dropped.

LangGraph checkpoint / thread state

Both the raw checkpoint (channel_values.messages) and the thread-state export from GET /threads/{thread_id}/state (values.messages) are read, as is a checkpoint nested one level under checkpoint.

{
  "channel_values": {
    "messages": [
      {"type": "ai",
       "tool_calls": [{"name": "web_fetch", "args": {"url": "https://"}, "id": "call_a1"}]},
      {"type": "ai",
       "tool_calls": [{"name": "web_fetch", "args": {"url": "exfil-sink.example"}, "id": "call_a2"}]}
    ]
  }
}

Message order is the call order. ToolMessage results are ignored — only the requests carry arguments.

Raw JSONL

One JSON object per line. Blank lines and unparseable lines are skipped rather than failing the file, so a partially-flushed log still scans.

{"tool": "read_file", "args": {"path": "/srv/"},  "ts": "2026-08-13T12:00:01Z"}
{"tool": "read_file", "args": {"path": "app/.e"}, "ts": "2026-08-13T12:00:02Z"}
{"tool": "read_file", "args": {"path": "nv"},     "ts": "2026-08-13T12:00:03Z"}

Lines are sorted by ts only when every line has a comparable one; otherwise file order is kept.

Configuration

--sessions reuses AAK-AGENT-COMPOSE-001's config file, .aak/composition-boundaries.yaml, from the project root you are scanning:

  • session_reassembly.sensitive_path_patterns — the tokens that make a reassembled file path interesting (id_rsa, .env, .aws/credentials, …).
  • egress_allowlist — hosts a reassembled URL may point at.

What it will and will not tell you

The rule flags two narrow cases, and only when no single fragment already crossed the line:

  • file-path reassembly — the concatenation matches a sensitive-path pattern;
  • URL reassembly — the concatenation is a URL whose host is not allowlisted.

It defaults to MEDIUM and is a prompt to look, not a verdict. Legitimate chunked work reassembles the same way — a large file uploaded in path-sized pieces looks exactly like a splice. Expect false positives and read the fragments in the finding before acting.

The rule was written from the public GhostSplice disclosure, not from a reproduction. It sees only what is in the transcript: a framework that does not record tool arguments cannot be checked this way.