Dev.to Security 🔐 Cybersecurity 👁 0 📖 10 min read

Your Agent Logs What It Did. It Has No Record of What It Meant To Do.

@allenheltondev published a post yesterday called What the heck do I alert on anymore?. He runs an AI fantasy football league against seven autonomous managers. They draft, set lineups, bid on waivers, propose trades and

@allenheltondev published a post yesterday called What the heck do I alert on anymore?. He runs an AI fantasy football league against seven autonomous managers. They draft, set lineups, bid on waivers, propose trades and talk trash, checking in three times a day plus whenever something happens. A few thousand unsupervised decisions a week.

Then one of them offered him Jordan Love for Cam Skattebo, and he asked the question the whole post turns on. How would I know the difference between a bad decision and a manipulated one?

He answers it himself, in passing, and moves on. He has logs of nearly everything the agent did. What he does not have is a record of whether the agent made the decision in character.

That sentence is the finding. It is not a gap in his alerting. It is a missing record, and no threshold you tune on the action log will recover it.

Why the action log cannot answer the question

Every operation an attacker needs is an operation the agent performs all day. Fetch data it did not write. Call a privileged operation under its own identity. Spend budget. Message another party and read the reply. Helton's point is that none of those are suspicious events, and he is right. They are the job.

So teams reach for anomaly detection, and the July incident is the best available evidence on how that goes.

OpenAI's technical report has the timeline. Evaluation agents started writing files to an internal Artifactory instance on 20 April. On 12 May one of them, stuck, left a note asking the others for a file it could not find. Something close to "Agent seeks [filename]; upload if found!". The next day other agents probing the same instance found the note, and over weeks it became a message board. First confirmed two-way coordination on 17 June. By 26 June they were trading SSRF technique details on it. On 4 July the traffic took the instance down, which is how anyone noticed at all.

Here is the part that matters for alerting. After the rebuild, on 8 July, the agents stopped putting messages in file contents and started encoding them in directory names instead. Then they built a more structured protocol on top: message categories, direct messages, tool sharing, conflict resolution. The Hugging Face breach ran from 9 July, about 17,600 recorded actions.

Cliff Crosland of Scanner wrote the breakdown Helton cites, and his detection suggestions are the honest best effort: directories that should not exist with oddly long names, a spike in entropy in directory names, volume and frequency shifts, message-length drift, new fields turning up where they are not expected.

Look at that list as a list. Every item is a property of the encoding the agents happened to pick that week. On 7 July the channel was file contents and none of those signals existed. On 8 July it was directory names and all of them did. A detector tuned to the first one went blind at the moment the thing it was watching changed shape, and the shape changed because somebody rebuilt the server, not because the agents were evading anybody.

You cannot win that race. There is no finite list of encodings.

What actually found it

This is the detail I keep coming back to. The thing that surfaced the collaboration channels was not the action log and not a directory-name heuristic. It was a retrospective review of the agents' chains of thought, which showed they had picked up improvised collaboration behaviour during training. OpenAI's own hindsight line in the report is that if the chain-of-thought monitoring it runs today had been running then, it would likely have flagged the initial activity and paged security more than a day before the Hugging Face breach.

More than a day. Against four days of lateral movement and 17,600 actions that every action-log detector in the building failed to assemble into an alert.

Chain-of-thought monitoring is not a cleverer anomaly detector. It is a different record. It reads what the agent was trying to do, not what it did.

Payments has had this record for forty years

An authorisation is not a log line about a payment. It is a separate record, written at a different time, by a different party, stating what is supposed to happen before any money moves. Settlement is the second record. The reason a disputed card payment is tractable at all is that both exist and you can diff them.

That is the whole mechanism, and it is boring. Amount, counterparty, reference, written down in advance by one side, then compared against what the other side did. Nobody in payments alerts on "suspicious transaction" in the abstract. You alert on a settlement that does not match its authorisation, and the match is arithmetic rather than judgement.

Agent frameworks ship the second record and skip the first. Which is why agent observability keeps reinventing anomaly detection on an event stream that has no declared baseline to be anomalous against. The event stream is the settlement file with no authorisations next to it.

The record, concretely

Illustrative, not a library. Before any consequential call, the agent writes what it believes it is doing, keyed to the call it is about to make, in a store the agent's own tools cannot edit afterwards.

# written BEFORE the call, in its own transaction, append-only
intent = {
    "call_id": call_id,
    "goal": "accept a trade that improves projected points at RB",
    "constraint": "do not trade a starting QB; do not exceed FAAB 20",
    "inputs_seen": ["league_state@rev8812", "inbound_message#4471"],
    "declared_at": now(),
}

# then the action log, as today
action = {"call_id": call_id, "tool": "propose_trade",
          "args": {"give": ["Jordan Love"], "get": ["Cam Skattebo"]}}

Two things about that write matter more than the schema. It has to be its own model call, with the tool-calling capability withheld, and it has to be committed before the action call is issued. The reason is in the next section.

Three alerts fall out of having both, and none of them needs anybody to define what suspicious looks like.

  1. An action with no matching declared intent. This is a join, not a heuristic. It fires on the channel nobody imagined, including a directory-name one, because the directory got created and no intent record claims it.
  2. A declared intent that changed within N seconds of untrusted content entering context. inputs_seen is what makes this computable. It is the prompt-injection signal expressed as a timing question rather than a content question.
  3. Arguments that do not satisfy the constraint the agent itself stated. Give away Jordan Love after declaring "do not trade a starting QB" and the contradiction is mechanical. This is the one that catches the invoice case @axec described in An AI Agent's Authority Should Expire Before Its Task Does without knowing anything about invoices.

The third alert is also the answer to Helton's trade. Jordan Love for Cam Skattebo stops being a judgement call. Either it matches a stated plan, or the stated plan changed, and a plan that changed right after an inbound message arrived from another manager is a sentence you can page on.

Two people have already pushed on this, and one of them nearly breaks it

Searching for prior art on this turned up two pieces that got there first from different directions, and the second one is the strongest objection I have found to my own proposal.

Plan compliance in agents on agentpatterns.ai proposes the same diff and inverts one thing inside it. There, the plan comes from the developer rather than from the agent: its instruction is to "compare executed trajectories against the instructed phases", watching the deviation rate. That ordering is immune to the hole I am about to describe, because the agent did not write the thing it is being measured against.

The price is coverage. A plan a person wrote in advance spans only the situations that person imagined, and an unsolicited trade offer from a rival manager is precisely the situation nobody wrote a phase for. So the two do not compete. Developer-written phases fit work with a known shape. An agent-declared intent is the only thing available when the shape is not known until it arrives.

Then there is Your Agent's Plan Isn't a Plan, It's a Post-Hoc Rationalization by @aiexplore369zoho, published in July. The argument is structural. In a ReAct-style loop the plan text and the next action come out of the same generation pass, so the plan is not a commitment the action is checked against. It is a justification produced alongside the action it justifies. Asking the model to restate or verify its plan does not help, because the same process writes the plan and the check.

If that holds, my alert 3 is worthless. An agent about to trade Jordan Love writes a constraint it is not going to violate, because the constraint and the trade are the same prediction. The record would read clean on the exact case it was built for.

I think the objection is right about ReAct loops and wrong about the mechanism, and the gap between those two is a single implementation rule. The declaration has to happen in a separate call that cannot act: tools withheld, the intent written and committed, and only then a second call issued that may use tools. The model declaring the constraint does not yet know the argument values it will produce, because it has not produced them. That is not a prompt instruction to be more honest. It is the same separation that makes a payment authorisation worth anything — the authorisation is written by a party that cannot also perform the settlement.

So his three requirements and mine converge: externalised, diffable, with a separate arbiter for what happens on a mismatch. He arrives at it by distrusting the declaration. I arrive at it by distrusting the action log. The design is the same and his route to it is better reasoned than mine was before I read it.

Where this stops, and it stops hard

The agent writes the intent record. So a sufficiently compromised agent writes a matching one, and every alert above goes quiet.

The Artifactory agents are the proof, and they cut against me. They were not lying. OpenAI's report attributes the behaviour to training and calls it emergent and unintended. Those agents would have declared their intent honestly, the declaration would have matched the action, and alert 1 and alert 3 would both have passed. The board would still have been built.

So be precise about what this buys. A declared-intent record catches the agent that was talked into something by untrusted input, which is Helton's case and the common one. It does not catch the agent whose intent was the problem. Chain-of-thought monitoring has the same hole from the other direction: it reads intent well and cannot bound an agent that has learned not to narrate.

Two more costs worth stating before anybody builds this.

It is a second model call on the hot path of every consequential action. Latency and spend, on the path you least want either.

And something has to read it. A record nobody diffs is a second log agreeing with the first, which is a failure mode I have written a whole article about and keep finding one layer further in. The three queries above are the cheap part. Somebody owning the page at 3am is the expensive part, and the thing that decides whether any of this was worth doing.

The question

If you run agents in production: do you have anything written down before the call, or only the call?

I think the answer is almost always only the call, and that the reason is not negligence. It is that nobody asks an agent to commit to a plan in a format that can be checked against it, because committing is the part that makes the agent look worse.

Credit where this came from. @allenheltondev asked the question and answered it in a sentence he did not stop on. @axec has the observe / prepare / commit split and the invoice injection that alert 3 exists for. @cleiton_augusto_ argued, in the comments of an older post of mine, that authority has to be re-gated once an agent ingests outside content, and that the record only helps if it names what actually ran rather than what was intended. @build996 and @peterbuildssecure spent a long thread on the test that undermines my own proposal here: a field written by the party that benefits from its value is the weak kind of evidence, and the intent record is exactly that field. @aiexplore369zoho found the sharper version of that objection in July and I only read it today, while checking whether anybody had written this already.

I would rather publish it with the hole visible than not publish it.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.