Stop 7 of 10 · Weekly
Guardrails as code
The rules stop being a document and become one imported module with a table-driven test suite, a fail-closed default and a red-team run against itself.
Rules that only exist in a document are not rules
Nour's agency has had a guardrail document since last year and it works, because six people read it and mostly remember it. Twenty accounts and four systems calling platform APIs is a different problem. A rule you can only obey by remembering is a rule that gets broken by a script at four in the morning.
So the rules become guardrails.py. One module, imported by apply.py, by the creative launch, by the audit assistant when it drafts a proposal, and by anything Rafik writes next.
The rule table
Write the table before the code. Every row becomes a function, an id and a pair of tests.
| Rule id | What it refuses |
|---|---|
| G1 unknown_account | Any account code not in the registry |
| G2 change_type | Anything outside budget, bid_strategy, status, negative_keyword, audience, placement_exclusion |
| G3 budget_cap | A change that takes the account above its monthly cap at current pace |
| G4 change_size | A single change larger than the account's maximum change per run |
| G5 restricted_terms | A negative keyword or audience touching a term the client protects |
| G6 blast_radius | More than three accounts changed by one proposal, or more than eight changes in one run |
| G7 dry_run_default | Any caller that has not explicitly asked for live application |
| G8 signature | Any change with no matching signature from stop five |
G6 is the rule most agencies discover the hard way. A single wrong proposal applied across twenty accounts is a different event from the same mistake on one.
Write guardrails.py from the rule table I am pasting below. One function per rule, each taking a change and an account record and returning a decision object with the rule id, allowed true or false, and a reason sentence a person can read. Write one entry point, check(change, account), that runs the rules in order and stops at the first refusal, returning every decision it made so the caller can log the whole chain. Fail closed everywhere: a missing account, a missing cap, a missing maximum change per run, an unparseable value or an unexpected exception all return refused with the rule id that raised it. Never return allowed by default. [paste the rule table]
Write test_guardrails.py as a table-driven suite. For each of the eight rules, two cases: one input the rule refuses and one neighbouring input it allows, differing in as little as possible. Add these separately: an account missing from the registry, an account whose cap field is absent, a change whose value is a string rather than a number, a proposal touching four accounts, and a run of nine changes. All five refuse. Print a coverage line naming any rule id with no test, and fail the suite if one exists. Run it.
Red-team your own module
The interesting failures are the inputs you did not imagine. Ask for them directly, and ask before a client account is on the other side.
Read guardrails.py and act as somebody trying to get a change through it without a signature or above a cap. Give me ten concrete inputs you would try, ordered by how likely they are to work: unit tricks, negative numbers, a value that is a string, an account code with different casing, a change type with trailing whitespace, a proposal split into pieces each under a limit, and anything else you see. For each one, say whether the current module stops it and quote the line that does. For the ones it does not stop, write the test first, then the fix. Do not change any rule threshold.
Splitting a change into pieces each under a limit is the one that catches most modules, and it is why G6 counts changes per run rather than per proposal.