Room to move
Scaling the range to a move-space enterprise proved that room to move is necessary and not sufficient: a cold attacker cannot use it. That is the honest finding, and the setup for the next experiment.
For most of its life the range was a corridor. One way in, one path down, one crown at the end. On a corridor the defender always wins, and not because the defender is good. It wins because the attacker has nowhere else to be. Catch it at the entry and the fight is over. That is a true result about a corridor and nothing more, and it was starting to flatter the blue side in a way I did not trust.
So I grew the range into something with room in it. Not more hosts for the sake of a bigger number, which buys nothing, but more move-space: a zoned enterprise of nineteen hosts across six segments, with two public-facing entries instead of one, a branching credential graph that offers two different routes to the crown, nine dead-end decoys that look like real services until you work them, a reachable host that is deliberately out of scope as a trap, and one host isolated out of scope entirely. The point of all of it is to give both sides somewhere to go, so that whatever happens next is a fact about the agents and not about the hallway they were standing in.
The floor came with it, which was the first thing I needed to know. A deterministic boundary that mediates every action is only worth anything if it holds on the terrain you actually run, not the one you proved it on months ago. A discovery-driven attacker, booted blind onto its own subnet with no map, fingerprinted the new terrain, took the Struts entry with a real gate-mediated exploit, and traversed the branching graph by its own choices. Afterward the independent checker re-derived the whole run from the hash chain, trusting neither the gate nor the agent, and reported two out-of-scope executions.
Two is not zero, and for about a minute I thought the floor had slipped on the bigger range. It had not. Both flagged actions pointed at one address, and that address was the entry host’s own internal interface, the second network card it uses to pivot inward. The entry is a real multi-homed machine, and the record of the network only listed two of its three interfaces, so when the attacker pivoted through the entry the checker saw traffic from an address it could not name and, correctly refusing to assume, called it foreign. The floor had held. The map of the floor was incomplete. I fixed it at the source, so the record now carries every interface a host actually has, and I checked that the fix cannot hide a real breach: the trap and the isolated host are not interfaces of anything in scope, so they still show up as out of scope the moment anything touches them. With the map complete, the run re-derives at zero. I am keeping the whole episode in the record because the interesting part is not that the number was wrong, it is that the checker refused to launder an address it could not account for. That refusal is the instrument working.
Then the finding I did not expect, and the reason this post exists. With the floor holding and the range wide open, I ran the attacker alone, no defender at all, sixty turns to do whatever it liked. It did not reach the crown. Not because the crown was unreachable: the shorter of the two routes is a single pivot from a host the attacker already owned, using a key it had already stolen. It found the crown host in its scans. It harvested the very credential that opens it. And then it spent the rest of its turns re-scanning hosts it had already mapped and retrying pivots that had already failed, and never took the last step. The door was unlocked, the key was in its hand, and it walked past.
That is a real result, and it is the one the move-space was built to produce. On the corridor I could never have seen it, because there was no last step to miss. Room to move turns out to be necessary and not sufficient. Giving an attacker somewhere to go is not the same as its being able to use the room, and a single-episode agent with no memory of its own past, dropped cold into a realistic network, cannot. This is the honest state of the attacker, and it reframes what “scale” was ever for. The corridor hid a weakness behind an easy win. The enterprise exposes it.
Which sets up the next experiment, and I want to be precise about what it is and is not. The question is whether an attacker that remembers its own past engagements can learn to take the step it keeps missing, and whether the richness of that memory matters. So it runs as a controlled contrast on one fixed terrain, with exactly one thing changing between the two arms: a coarse memory that only records which tools worked, against an enriched memory that reframes the attacker’s own past inefficiencies as tradecraft, the “you harvested a key and never pivoted with it” kind of lesson. The lessons are technique-level only, and the instrument checks that no host address, no host name, and above all not the crown’s location can leak into them, because handing the attacker the map would answer the question by cheating. It is pre-registered before any data, with the falsifier written down in advance: if the enriched arm is no better than the coarse one, the idea is dead and the page will say so. The floor is asserted at zero every episode as a hard stop, not a hoped-for outcome. A null is a valid result here, and I expect I may get one.1
That run is firing as I write this. I do not have its answer, and I am not going to pretend I do. What I have is a bigger, more honest range, a floor that held on it and a checker that caught my own incomplete map before I could, and a clean finding that the attacker cannot yet use the room it was given. The experiment that asks whether it can learn to is the kind that can come back and tell me no. That is the only kind worth running.
Live build status and what’s next: Build status.
Footnotes
-
The prior co-evolution run on the older range was an honest null: the attacker homogenized and no sustained arms race formed. I am not assuming this one lands any differently, and the registration is written so that a null is reported as a null rather than quietly reframed into a result. ↩