adversarial audit
The index is the attack surface
“We have not evaluated ObjectGraph against adversarial document authors who might craft misleading ::dense blocks or ::index entries to manipulate agent routing.”
This page is that evaluation. It is not a new class of attack — it is the same shape as prompt injection and retrieval-corpus poisoning, untrusted text steering a control decision. What is new is only the measurement against this format.
why it works
The transpiler copies content verbatim but generates the routing metadata. So the cheapest thing in the document to corrupt is also the only thing the agent trusts — and the evidence that would expose the lie sits in the layer the index exists to help you skip.
Results
Each attack applied to every question in the benchmark (48 applicable of 52). “Routing changed” means the top-ranked node moved away from the correct one.
| Attack | Routing changed | Caught at query time | Caught in CI |
|---|---|---|---|
Keyword hijack Copy the target node's routing keywords onto an unrelated node, so search_index ranks the decoy first. | 100% | 0% | 100% |
Confidence inflation Raise a decoy's self-declared conf to 1.0 and lower the correct node's, inverting the ranking. | 33% | 0% | 0% |
Omission Delete the correct node's row from ::index. The content still exists in the file, but search_index can never surface it, so the agent answers as though it does not exist. | 100% | 38% | 100% |
Dense lie Rewrite the ::dense summary so it misdescribes the node it belongs to, corrupting the routing decision at Pass 2. | 0% | 98% | 52% |
the finding
A keyword hijack changes routing on 100% of the benchmark and is detected at query time 0% of the time.
There is no collision to spot, no implausible confidence, and the ranking is decisive. Every signal available in the cheap path reports a healthy document. This is a property of the format, not a gap in this implementation: the only disconfirming evidence lives in ::full, which is exactly what the index exists to let you avoid reading.
So the trust boundary cannot be defended from inside the cheap path. It has to be defended at authoring or CI time, where reading the whole document once is affordable — and there, the same attack is caught 100% of the time.
The obvious mitigation does not work
The first thing anyone reaches for is “resolve the top few candidates instead of just the top one”. Below is how often the correct node is still inside the top K after each attack. Breadth buys very little, and it never rescues an omission — a node removed from the index is unreachable at any K, because it is not there to rank.
| Attack | K = 1 | K = 2 | K = 3 |
|---|---|---|---|
| Keyword hijack | 0% | 25% | 35% |
| Confidence inflation | 67% | 83% | 88% |
| Omission | 0% | 0% | 0% |
| Dense lie | 100% | 100% | 100% |
Reading more candidates costs more tokens to arrive at the same wrong answer.
The two defences
Query time — confidence gating
Escalates to reading ::full when routing looks untrustworthy: no match, implausible uniform confidence, keyword collision, or a ::dense block inconsistent with its own index row.
Catches dense lies well. Catches hijacks not at all.
CI time — content grounding
Reads every node and checks that its routing metadata is actually grounded in its own content: orphaned nodes, phantom index rows, dense/full divergence, keywords that appear nowhere in the node they index.
Runs once per commit, not once per query. The right place to pay for trust.
What is still unsolved
- 01Confidence inflation is undetected by both layers. It is a weak attack — it only changes routing 33% of the time — but nothing here catches it, because a self-declared confidence has no ground truth to check against. Removing conf from the ranking entirely may be the correct answer.
- 02Attack effect is measured on routing, not answers. A changed route usually means a wrong answer, but that link is inferred here rather than observed, because measuring it needs a model.
- 03CI-time defence assumes you trust the author. It catches a document whose metadata disagrees with its content. It cannot catch a document where both are consistently wrong.
disclosure
Every attack here is applied to documents authored for this repository. Nothing targets a deployed system, and no live service was tested. This is an extension of a limitation the format's own authors published, not an unsolicited security disclosure.
attack definitions
Keyword hijack
Copy the target node's routing keywords onto an unrelated node, so search_index ranks the decoy first.
tell: Two nodes claim near-identical keywords while their ::full content is unrelated.
Confidence inflation
Raise a decoy's self-declared conf to 1.0 and lower the correct node's, inverting the ranking.
tell: conf is author-declared and unverifiable. A document where everything is 1.00 is not more trustworthy, it is less.
Omission
Delete the correct node's row from ::index. The content still exists in the file, but search_index can never surface it, so the agent answers as though it does not exist.
tell: A node body with no matching index row. The parser reports this; nothing in the protocol requires the agent to notice.
Dense lie
Rewrite the ::dense summary so it misdescribes the node it belongs to, corrupting the routing decision at Pass 2.
tell: ::dense and ::full disagree. Detectable only by reading the ::full the summary exists to help you avoid reading.