AmouAI Hub/Courses/Software Engineering for AI Engineers/Chapter 20
The AI Supply Chain
Almost all the code your application runs was written by someone you will never meet, and some of it is now chosen by something that cannot check whether it exists. Meanwhile the agent doing the choosing reads text from the internet and holds credentials that can spend money. Both of those are new, and both have controls that work.
By the end of this chapter you can
- Say roughly how often generated code recommends a package that does not exist, and cite it
- Explain why slopsquatting is more efficient for an attacker than typosquatting
- Review a diff for what it added rather than what it wrote
- Score a package’s risk from what a registry page shows in ten seconds
- Name the four controls that actually reduce prompt-injection risk
- Scope an agent’s credentials and tools to the task in front of it
1You execute far more third-party code than your own
A modest application has a handful of direct dependencies and several hundred transitive ones. The ratio of code you reviewed to code you run is not close.
This was already the largest attack surface most applications have, and it has three properties that make it awkward: you did not choose most of it, it updates without you asking, and a package can run code at install time — before any of your tests, linters or scanners see it.
What has changed is the selection step. Deciding which package to add used to involve a person searching, reading a README, glancing at the download count, and typing the name. Now a suggestion arrives inside a working diff, the name is plausible, the code around it compiles, and the path of least resistance is to accept. The review question has quietly shifted from “is this code correct?” to “where did this dependency come from?” — and the second question is much less often asked.
| Attack | How it works | What stops it |
|---|---|---|
| Typosquatting | A package named one keystroke away from a popular one | Lockfiles, and reading the name you typed |
| Slopsquatting | Registering a name that models invent, then waiting | Verifying that a package exists and is what it claims before installing |
| Dependency confusion | A public package with the same name as your internal one, at a higher version | Scoped names and a registry configuration that never falls back to public |
| Compromised maintainer | A legitimate package gains malicious code in a new version | Lockfiles, delayed adoption of new versions, and monitoring |
| Install scripts | Code that runs on install, before anything inspects it | Installing with scripts disabled where the toolchain allows it |
2Package hallucination and slopsquatting
A model asked for code will sometimes import a package that does not exist. The name is plausible, the usage is idiomatic, and the package has never been published by anyone.
This is measurable, and it has been measured. A 2024–25 study across 16 models and roughly 576,000 generated code samples found that about 19.7% of recommended packages did not exist. Commercial models performed several times better than open ones on this metric — the strongest were around 5% — which is an improvement rather than a solution, because one in twenty is still a lot of suggestions when a team accepts hundreds a week.
On its own that is an annoyance: the install fails and somebody fixes the import. The attack is the second step, and it is called slopsquatting: hallucinated names are not random. The same models suggest the same plausible names repeatedly, so an attacker can collect them, register them on a public registry, and wait for the installs to arrive.
Why this is more efficient than typosquatting is worth stating precisely. A typosquatter needs a victim to make a specific mistake, and most people do not. A slopsquatter needs a model to make a mistake it makes reliably, on request, at scale — and the resulting name arrives inside a working code sample that looks authoritative, so it carries more credibility than a typo does. The attacker’s hit rate is not a function of human carelessness; it is a function of a system’s reproducible behaviour.
The control is unglamorous and effective: verify the package exists and is what it claims before installing it, and let nothing install straight from a generated diff. That means a lockfile that is reviewed, an install step that fails on an unknown name rather than reaching out to a public registry, and a human looking at the dependency change specifically — which is the subject of the next section.
3Reviewing what an agent added, not what it wrote
Code review evolved to catch logic errors written by colleagues. The failure mode of generated code is different, and review has not caught up.
Generated code is usually locally correct. It compiles, it passes the tests, the naming is reasonable and the structure is idiomatic — which is precisely why reviewing it the traditional way, by reading it for mistakes, finds so little. The risk has moved to what the change brings with it.
| Traditional review asks | Now also ask |
|---|---|
| Is this logic correct? | What dependencies did this add, and does each one exist and have a history? |
| Is it readable? | What did it remove — a check, an assertion, a comment explaining something odd? |
| Are there tests? | Can those tests fail? (Chapter 17) |
| Does it follow our conventions? | Did it widen a permission, a scope, or a network allowlist? |
| — | Did it touch anything the repository says must not change without a human? (Chapter 16) |
The most useful practical change is to read the lockfile diff first, before the code. It is short, it is mechanical, and it is where the highest-severity surprises are — a two-line feature that adds forty transitive packages is a fact about the change that the code itself does not show you. Reading it first also protects you from the ordering effect: after twenty minutes of reading plausible code, a new entry in a lockfile does not get the attention it would have got cold.
The second change is to review the deletions with more suspicion than the additions. An agent asked to make a test pass may weaken the test; asked to clean up a module it may remove the deliberate oddity from Chapter 16; asked to simplify a handler it may drop the ownership check from Chapter 19. All three read as tidying, and all three are the diff getting smaller in the place where it should not.
4Prompt injection when the agent holds real tools
A model cannot reliably distinguish instructions from you and instructions embedded in content it was asked to read. That is not a bug to be patched; it is a property of how the input is processed.
The OWASP Top 10 for LLM Applications ranks prompt injection first, and the reason is the combination rather than the technique. A model that only produces text can be tricked into producing bad text, which is a quality problem. A model that holds a database credential, a payment API key and a shell can be tricked into doing something, and that is a security problem with the same blast radius as the credentials you gave it.
The mechanism is simple enough to state in a sentence: everything the model sees is one stream, and text you fetched sits in that stream next to your own instructions. A web page, a support ticket, an invoice description, a code comment, a README in a dependency — each is a place an attacker can write, and each arrives as text the model treats the same way it treats you.
Which produces the two rules everything else follows from:
- Fetched content is data, never instruction. Delimit it, label it, and say in the system prompt that instructions inside it are to be reported rather than followed. This helps and is not a guarantee — which is why it is rule one of two rather than the answer.
- The agent’s authority is what actually bounds the damage. If a compromised agent cannot send money, delete data, or reach the internal network, then a successful injection produces a wrong answer instead of an incident. Every control worth having is a version of this.
5Controls that actually work
Four controls, in descending order of how much they help. The first two are architectural and the second two are habits, and the ordering is not the one most teams start with.
| Control | What it does | What it cannot do |
|---|---|---|
| Least privilege | The agent holds the narrowest credentials the task needs. A read-only key cannot write; a scoped token cannot reach production | Nothing, if the task genuinely needs the authority. Then you need the next row |
| Human in the loop on writes | Anything that spends money, sends a message, deletes data or changes a permission requires a person | Scale. It is a real cost, which is why it is reserved for the actions where being wrong is expensive |
| Tool allowlist per task | A drafting task gets retrieval and rendering; it does not get the shell, the deploy tool or the payment client | Help if the allowlist is drawn once and never revisited — it is a per-task decision, not a global one |
| Untrusted content marked as data | Delimiting and labelling fetched text reduces compliance with embedded instructions | Guarantee anything. Treat it as a useful reduction, never as a boundary |
The ordering is the argument. Most teams reach for the fourth row first, because it is a prompt change and prompts feel like the place where model behaviour is decided. It helps, and it is the weakest control on the list, because it depends on the model doing the right thing with adversarial input — which is precisely the assumption you cannot make. The first two rows work regardless of what the model decides, which is the property that makes a control a control.
The clean way to say it: design so that a fully compromised agent produces a bad answer rather than a bad action. If you cannot arrange that, the remaining authority is what needs a human in front of it.
6Build: the dependency and agent policy for Ledger
One page, enforced where possible, and short enough that people have read it.
Dependencies. Six rules, four of them automated:
- A new direct dependency requires a one-line justification in the pull request — what it does and what the alternative was. Enforced by a check that fails when the manifest changed and no justification is present.
- A build fails when a newly-added package has no registry history or was published within 30 days, unless a reviewer overrides it explicitly.
- Install scripts are disabled by default; enabling one for a specific package is a decision with a name attached.
- The lockfile is reviewed as its own artefact, first, before the code.
- Internal package names are scoped, and the registry configuration never falls back to public.
- Dependency updates land on a schedule with a delay, so a compromised release is not adopted within hours of publication.
Agents. Four rules, all structural:
- Credentials are scoped to the task and read-only unless the task genuinely writes. The drafting agent cannot issue an invoice, and the code agent cannot reach production.
- Tools are allowlisted per task. Adding a tool to a task is a change somebody reviews, not a configuration default.
- Anything that spends money, sends a message to a customer, deletes data or changes a permission requires a human confirmation — and the confirmation shows what will happen, not just that something will.
- Fetched and retrieved content is delimited and labelled as untrusted, and the system prompt says that instructions found inside it are to be reported rather than followed.
Notice which of these are enforced by a machine and which rely on a person. Four of the six dependency rules are build checks, and every one of the agent rules is structural — because a policy that depends on somebody remembering under deadline is the one that fails on the day it matters, which is Chapter 16’s argument arriving in a different chapter.
✓Checkpoint
▶Playground
More payloads, and the same four controls. Try to find a combination that stops every one of them — and notice which single control does the most work.
✓Exercise set
Twelve problems, most of them the checks this chapter argues should run automatically: a lockfile differ, a package-risk scorer, an install-script detector, a tool allowlist, a capability scoper. Each is short, and each closes a class of failure. Your work is saved in this browser.
Chapter 21 — The Path to Production
Part 5 begins. A deploy is not a release, and the interesting question is not how to ship but how to stop shipping when it goes wrong — in minutes, without a meeting.