Academyby Dasow

Stop 5 of 10 · Weekly

Proposals to applied changes

One proposal format, a signature over the exact bytes, an apply step that refuses anything unsigned, and a rollback that was written before the change went out.

Watch first · 0:55

The only file that writes

Everything so far reads. This stop builds the one path from a proposal to a live change, and it is deliberately narrow: one format, one signature check, one function that calls a platform write endpoint, one log.

A proposal is a JSON file, one change per entry, written by the audit assistant or by a person.

Field Why it is there
id, account_code, platform, change_type What is changing and where
target, from_value, to_value The rollback is written by reading these backwards
evidence, dollars, risk What a person needs to sign or refuse in a minute
expires_on After this date the rows behind it are too old to act on

from_value is not optional. A proposal that does not state the current value cannot be reversed and cannot be verified, and it is also the field that catches the proposal written against a snapshot from before somebody else changed the account.

Build the proposal schema and the signer
Write proposal.py and sign.py for our agency systems repository.

proposal.py defines the schema above, validates a proposal file, and refuses one with a missing from_value, an expires_on in the past, or a change_type outside our list: budget, bid_strategy, status, negative_keyword, audience, placement_exclusion.

sign.py takes a proposal path and an approver name. It prints the proposal as a human would read it, asks for confirmation, then writes approvals/<id>.json containing the approver, an ISO timestamp and the SHA-256 hash of the exact proposal file bytes.

Write test_sign.py: a valid proposal signs and verifies, a proposal edited by one character after signing fails verification, a proposal with no from_value is refused before signing, and an expired proposal is refused. Run the tests and show me the failure message on the tampered case.

Apply, and its default

apply.py defaults to a dry run. Live application takes an explicit flag, the writer credential from stop one and a matching signature. The dry run prints the exact platform call it would make, which is what you read before you type the flag.

Build the apply step
Write apply.py. It takes a proposal path. It verifies the signature hash matches the file bytes, loads the guardrail module and re-checks every change against it, then either prints the platform calls it would make, or with the --live flag, executes them.

For each change: read the current value from the platform first, and refuse if it does not equal from_value. Then call the write endpoint using the current Google Ads API or Meta Marketing API mutate shape, checked in the official documentation rather than written from memory. Record the platform response id.

Append one line per change to changes.jsonl: timestamp, proposal id, account, change type, target, from, to, approver, signature hash, platform response id, and outcome. On an error mid proposal, write everything already applied to the log, mark the proposal partially applied and exit non-zero.

Write test_apply.py against a fake platform client: unsigned refuses, tampered refuses, mismatched from_value refuses, guardrail failure refuses, dry run makes no calls, and a mid-proposal error leaves a log with the applied changes recorded.
Generate the rollback before you apply
Read changes.jsonl for proposal [paste the proposal id] and write the inverse proposal that returns every applied change to its from_value. Use the same schema. Set expires_on to seven days from today. Do not apply anything. Tell me which changes cannot be cleanly reversed and why, and what evidence I would need before deciding to leave them.

Run that prompt before the apply, not after. A change whose rollback you cannot write is a change you do not understand well enough to make.

Quick check

Try it

Report a bug or share feedback