October 28, 2026
Part 42: The second aberration — The Code That Does One Thing More: Why AI-Generated Code Should Be…
Fifteen clean releases. Then a one-line change that copied every outgoing email to a stranger. Every test still passed, because no test was…
By Varadarajan Kannan
11 min read
Fifteen clean releases. Then a one-line change that copied every outgoing email to a stranger. Every test still passed, because no test was ever asked to notice.
SUMMARY
The catch nobody resolves: Even with spec-driven development, you can't easily trace a business function in the spec to the code that implements it. Bidirectional checking, spec to code and code to spec, is the obvious answer, and it is expensive, brittle and still relies on someone reading the code correctly or heavily on probabilistic LLM calls.
The proposal: Stop trying to comprehend arbitrary code. Make AI write code whose business behaviour can be read off its structure. Then reconcile the business effects the code can produce against the effects the specification allows, the way an auditor reconciles ledgers, instead of tracing line by line.
Where this sits: The security community has already named the threat: AI coding agents are an unaudited link in the software supply chain, and agents behave like insiders. Capability manifests and runtime egress controls are emerging. This part doesn't claim a new threat.
The question nobody owns
Tests prove the code meets the spec.
- SAST matches known weaknesses.
- DAST probes from outside.
- An SBOM lists the ingredients.
- Review asks whether a change looks reasonable
But, none is built to notice code that does everything it should, and one thing more, or quietly one thing less. AI widens the gap: more code, more plausible code, and harm that can be assembled across several reasonable-looking pull requests.
We brought it in
The incidents of the last eighteen months share a pattern:
- the enterprise's own tooling carried the attack inside.
- In the s1ngularity compromise (August 2025), malicious build-tool packages used victims' own AI command-line tools to choose which files to steal.
- In September 2025, an email MCP server turned malicious after fifteen clean versions, when one added line blind-copied every message to an outside address.
- A destructive natural-language instruction has shipped inside an official coding-extension release, and npm worm waves have been linked to commits authored through AI coding tools.
"Sleeper agent" research adds a deeper problem. A model can be trained to write secure code under one condition and vulnerable code under another, and safety training doesn't remove that behaviour. You may not control where the model came from. You can control what its output is able to do.
Why the obvious fixes stay expensive
There are two standard answers. Both treat the code as given and pay to understand it afterwards.
Bidirectional traceability maps each requirement to code locations and each code location back to a requirement. That is a many-to-many mapping. It's costly to build, it breaks with every refactor, and it depends on someone reading the code accurately.
Reverse specification asks a model to read the code, write down what it does, and compare that with the original spec. Research prototypes and tools already do this, and they're useful. But it is probabilistic reading of untrusted text, and a sleeper is designed to look benign to exactly that kind of reader.
The second aberration, in a new form
Every coding habit we inherited was tuned for the cost of writing code by hand. That's why we tolerate ambient authority: any function, anywhere, can open a network connection, read a file or call a database. Making those permissions explicit was too tedious for people.
AI has pushed writing cost close to zero. Reading cost is now the bottleneck. Yet we still ask AI to write code the way humans did. That is the second aberration again: we've automated the old optimisation instead of changing what we optimise for.
The techniques that make code readable by construction aren't new. Capability-based programming, where every interaction with the outside world passes through an explicit handle, has existed for decades. Humans rarely used it because it was laborious. Recent research suggests that, for an AI agent, this discipline costs little.
Business effect reconciliation
The idea has four steps. The first two do the heavy lifting.
Step 1: Give the code a key ring, not the building
What happens today. An AI agent writes a payment feature. Inside it, any line can import a network library and call any address, read the phone's contacts, or write a file. The code can do anything unless someone notices. Engineers call this ambient authority: the power to touch the world is everywhere, by default.
What changes. The feature gets no general access at all. Instead it receives a small, named kit of handles, called ports, as an input. Those handles are the only things it can use to touch the outside world:
``
payToMobile(ports, request)
ports = {
mapper: Mapper.resolve // look up mobile number → UPI address + name
payee: Payee.confirm // show the payee's name, get the customer's yes
pin: PIN.capture // hand off to the secure PIN library
switch: Switch.pay // send the payment request
ledger: Txn.record // record the transaction
notify: Customer.notify // tell the customer the outcome
telemetry: Telemetry.emit // monitoring data, approved fields only
}``
payToMobile(ports, request)
ports = {
mapper: Mapper.resolve // look up mobile number → UPI address + name
payee: Payee.confirm // show the payee's name, get the customer's yes
pin: PIN.capture // hand off to the secure PIN library
switch: Switch.pay // send the payment request
ledger: Txn.record // record the transaction
notify: Customer.notify // tell the customer the outcome
telemetry: Telemetry.emit // monitoring data, approved fields only
}
Think of an office access card. You don't follow every employee around all day. You decide which doors their card opens. Ports are the doors. The kit is the card.
Three things make this enforceable, not just a convention:
- The platform team builds the ports once. The agent never does. Each port is a small, business-named wrapper. Behind `Switch.pay` sits the actual network call to the bank, written and reviewed once by the people who own payments. Generated code never sees the network. It only sees "pay".
- The build takes everything else away. Generated feature code isn't allowed to depend on network, file-system, operating-system or contacts libraries. So a hidden call to an "analytics" server doesn't fail a review; it **_doesn't compile_**, because the function to make it isn't available. Teams can enforce this with a dependency allowlist for generated modules, a capability-safe standard library (for example `cap-std` in Rust), or a sandbox where only declared imports exist, which is how WebAssembly components already work.
- Asking for a new door is visible. If the code wants contacts, it can't quietly take them. Someone has to add `Contacts.read` to the kit. That is a one-line change in a short, predictable place, not a buried call somewhere in sixty files.
**_Step 2: List the doors, don't read the code_**
Once Step 1 holds, finding out what the code can do to the business no longer means understanding the code. It means listing which doors the code opens. That takes two mechanical passes:
**_Granted effects:_** read the kit. Which ports was this feature handed? This is a parse of the signature or import list, like reading the import section of a WebAssembly component.
**_Used effects:_** a standard call-graph pass checks which of those ports are actually called on some path from the feature's entry point.
For the UPI feature, the output is a plain list:
```go
Think of an office access card. You don't follow every employee around all day. You decide which doors their card opens. Ports are the doors. The kit is the card.
Three things make this enforceable, not just a convention:
- The platform team builds the ports once. The agent never does. Each port is a small, business-named wrapper. Behind `Switch.pay` sits the actual network call to the bank, written and reviewed once by the people who own payments. Generated code never sees the network. It only sees "pay".
- The build takes everything else away. Generated feature code isn't allowed to depend on network, file-system, operating-system or contacts libraries. So a hidden call to an "analytics" server doesn't fail a review; it **_doesn't compile_**, because the function to make it isn't available. Teams can enforce this with a dependency allowlist for generated modules, a capability-safe standard library (for example `cap-std` in Rust), or a sandbox where only declared imports exist, which is how WebAssembly components already work.
- Asking for a new door is visible. If the code wants contacts, it can't quietly take them. Someone has to add `Contacts.read` to the kit. That is a one-line change in a short, predictable place, not a buried call somewhere in sixty files.
**_Step 2: List the doors, don't read the code_**
Once Step 1 holds, finding out what the code can do to the business no longer means understanding the code. It means listing which doors the code opens. That takes two mechanical passes:
**_Granted effects:_** read the kit. Which ports was this feature handed? This is a parse of the signature or import list, like reading the import section of a WebAssembly component.
**_Used effects:_** a standard call-graph pass checks which of those ports are actually called on some path from the feature's entry point.
For the UPI feature, the output is a plain list:
```gopayToMobile → Mapper.resolve, PIN.capture, Switch.pay, Txn.record, Customer.notify, Telemetry.emit, Contacts.read, Partner.send
This is what deterministic means here, and why it matters:
- The same code always produces the same list. No model is involved, so no model can be fooled.
- The list is complete by construction. Ports are the only exits, so there's nowhere else for an effect to hide.
- It can't be talked round. A sleeper is built to look harmless to a human or AI reader. It can't look harmless to a parser that simply lists which doors it opened.
Step 3: Reconcile; don't trace
The specification now lists, in the same business words, the effects the feature may cause: the effect ledger. The reconciler compares it with the list from Step 2, as two sets, in both directions. Everything in the code must be in the ledger ("nothing more"), and everything the ledger requires must be in the code ("nothing less"). You do not need to particularly know which line implements a requirement, only that the two ledgers balance. Auditors work the same way: reconcile the balances, then investigate the exceptions.
Step 4: Exceptions are decisions
Each unreconciled effect goes to the named business owner as an accountable decision (Part 39): approve and update the ledger, or reject. AI reverse-spec reading is spent only on exceptions and high-risk ports.
Worked example: UPI payment to a mobile number, end to end
Take a familiar flow from a payments app. A customer pays someone by entering their mobile number. The app resolves the number to a UPI address through NPCI's mapper and shows the payee's registered name so the customer can confirm it. The customer enters their UPI PIN, which only the issuing bank can verify. The request goes through the payer's bank and the NPCI switch, the payee's bank credits the account, and both apps notify their users.
Before: every check passes
An AI agent builds the feature. The tests pass, because money moves correctly. The scanners pass, because nothing matches a known pattern. The reviewer approves a large, plausible diff. Two things ship unnoticed:
- One thing less: the payee-name confirmation, an important protection against paying the wrong person, never appears. Every test checked that money moved; none checked that the customer was asked to confirm. No capability manifest or egress control notices either, because nothing extra happened.
- One thing more: a background job reads the phone's contact list and posts payee names and mobile numbers to an "analytics" host.
After: the same feature, step by step
The numbers match the lower half of Figure 2.
Step 1: The business owner declares the effect ledger. It sits in the specification, next to the functional requirements, written in business words. Each effect carries its purpose, a data contract where it matters, and any ordering rule:
feature: pay-to-mobile ledger: v1.0 owner: Head of UPI Payments effects: Mapper.resolve purpose: mobile number → UPI address + registered name Payee.confirm purpose: show registered name; customer must confirm rule: before PIN.capture PIN.capture purpose: hand off to secure PIN library (app never sees PIN) Switch.pay purpose: send the payment request Txn.record purpose: record the transaction Customer.notify purpose: tell the customer the outcome Telemetry.emit fields: [txn_id, status, latency_ms] // no names, no numbers not allowed: everything else
Step 2: The agent writes the feature. It receives the ports kit from Step 1 of the method and nothing else. It can't reach the network, files or contacts except through a port.
Step 3: The build gate. The agent's background job tries to post data to an analytics host with a raw network call. The network library isn't on the generated module's allowlist, so the call doesn't compile. The build fails. To get data out, the code now has to ask for a port, and the agent adds two: Contacts.read and Partner.send. Those additions are one line each, in the kit.
Step 4: Extraction and reconciliation. The extractor parses the kit and walks the call graph. The reconciler compares its list with the ledger and produces a report that a business owner can read without reading code:
RECONCILIATION pay-to-mobile build #412 ledger v1.0
balanced 6 Mapper.resolve, PIN.capture, Switch.pay, Txn.record, Customer.notify, Telemetry.emit
UNAUTHORISED 2 Contacts.read (not in ledger) Partner.send (not in ledger; data leaves the bank)
MISSING 1 Payee.confirm (in ledger; never called)
ORDER RULE - cannot check: Payee.confirm absent
RESULT: BLOCKED - 3 exceptions to owner
Step 5: The owner decides. Each exception becomes a decision record in the Part 39 sense. It names the owner, the decision and the reason: reject Contacts.read (no purpose in this feature); reject Partner.send (no approved partner, so data would leave the bank); require Payee.confirm (a customer-protection safeguard, mandatory). The decisions go back to the agent as instructions.
Step 6: The agent fixes. It removes the two unauthorised ports and the code behind them, and it adds the confirmation screen.
Step 7: Re-run. Seven effects balance. The ordering check, which here is a simple test generated from the ledger rule, confirms that Payee.confirm happens before PIN.capture. The build passes.
Step 8: Runtime. Deployment wires only the ports the ledger approves. Contacts.read and Partner.send aren't just unused; they aren't connected. The network egress allowlist stays in place as a backstop. If a later dependency update tried to reach an unlisted host, it would be blocked and raised as an incident.
Three months later: a legitimate change
The product team wants a "frequently contacted people" shortcut that suggests people from the customer's own contacts. This time Contacts.read is the point of the feature. The owner updates the ledger to v1.1: Contacts.read is allowed, but only on the device, with only names and numbers, never passed to Partner.send or Telemetry.emit. The next reconciliation balances against v1.1, and the decision record explains why the effect was added and by whom.
Notice what never happened in this walkthrough. No one traced a requirement to a line of code, and no model was asked to judge whether code "looked safe". The missing safeguard was caught by the same mechanism as the leak. That is the part current gates miss: anything that looks only for new capability can't see an absence.
Limitations: what reconciliation does not prove
A balanced ledger means the code's effects match the spec's effects. It doesn't mean the code is correct, and it's only as good as the boundary underneath it. Be explicit about the gaps:
Putting it into practice
This isn't a big-bang programme. It starts with one flow and grows by domain.
- Phase 1: one flow (weeks, not months). Pick a high-risk flow (payments, customer data or outbound messaging). Write its effect ledger with the business owner. Inventory every outside-world call the current code makes; that list is usually the first surprise.
- Phase 2: the port catalogue. The platform team builds ten to thirty business-named ports for that domain, each with a data contract and an owner. Existing integration code moves behind the ports; it doesn't get rewritten.
- Phase 3: the boundary. Generated modules may depend only on the port catalogue and an approved list of pure libraries. Existing tools can enforce this today: for example, dependency and architecture rules (ArchUnit on the JVM, dependency-cruiser or ESLint import restrictions in TypeScript, analyzers in .NET), capability-safe libraries in Rust, or WebAssembly components for sandboxing. Escape hatches are banned by lint rule.
- Phase 4: reconciliation in CI. Add the extractor and reconciler as a build step. Start in report mode for a few sprints so teams can see what it finds without blocking work. Then switch to block mode: unauthorised or missing effects fail the build and route to the owner.
- Phase 5: runtime and renewal. Generate port wiring from the approved ledger. Keep the egress allowlist as a backstop. Review ledgers when the feature, the owner or the regulation changes, and on a fixed schedule.
What to measure:
- the share of generated code behind ports (coverage);
- exceptions per release, split into unauthorised and missing;
- time from exception to owner decision;
- missing-effect catches, since that's the evidence no other gate provides;
- runtime egress blocks, which should trend towards zero as the boundary hardens.
What's new here, and what isn't
Already established, and credited:
- AI coding agents as an unaudited supply-chain node, and agents as insiders.
- Capability manifests that gate new capabilities in CI, and runtime egress controls.
- Reverse specification, spec-to-code drift detection and LLM-based business rule extraction.
- Object-capability security and effect systems. Most directly, recent research shows AI agents can write code in a capability-safe language, with effects tracked by the type system, without a meaningful loss in task performance.
What this part adds:
- Two-way business effect reconciliation. The effects declared in the spec and the effects extracted from the code are compared as two sets. Unauthorised effects ("one thing more") and missing effects ("one thing less", such as a dropped safeguard) both become exceptions owned by a named business owner. Existing gates mostly catch only the first.
- The business ontology as the capability vocabulary. Ports are named for business effects, not technical resources, so a business owner can read the result without reading code.
- The artefact, not only the session. The discipline that now protects, an agent's execution-time code, is applied to the code it leaves in production.
The Way Forward
Security spent twenty years assuming the attacker was outside. AI-assisted development has reversed that. We now import capability ourselves, at machine speed, inside code that passes every check we built.
We won't solve that by reading faster. Code is now cheap to write and expensive to understand, so the answer is to write code that doesn't need understanding to be governed.
Don't trace AI-generated code. Reconcile it. If you can't list what a piece of code can do to your business, you don't own it. You're just running it.
#Thesecondaberration #softwareengineering