AI Agent Cost Drift: 0.35%/day Is Invisible to Your Dashboard A developer built drift_anchor_gate.py to detect slow AI agent cost drift that standard rolling baselines miss. Testing showed a 0.35% daily cost creep raised zero alarms over 60 days in four rolling detectors, while a frozen anchor caught it on day 9. The tool uses a frozen canary from day 0 to compare against, blocking cost growth that would otherwise go unnoticed. AI agent cost drift is the slow growth of your input floor — system prompt, tool schemas, CLAUDE.md, MCP servers — that a rolling baseline never catches, because the baseline climbs with it. drift anchor gate.py pins a frozen canary on day 0 and compares: a 0.35%/day creep raised zero alarms in 60 days ; the anchor blocked it on day 9 . Your fleet dashboard fires when today's average run jumps 20 percent above last week. Your context floor grows 0.35 percent a day. Those two numbers never meet. I built six 60-day worlds and ran four rolling detectors over them, each at the tightest threshold that stays quiet on a flat fleet, and the slow creep raised zero alarms in all four, across 60 days , while the floor went up 22.6 percent. A frozen anchor caught the same creep on day 9 . AI disclosure:I wrote drift anchor gate.py and make worlds.py with an AI assistant and ran them myself: Python 3.13.5, offline, standard library only, no network, no keys. Every number, exit code and sha256 below is pasted from a real local run. I ran the whole demo twice from a clean rm -rf worlds , and the two output.txt files are byte-for-byte identical sha256 1772e695cb75f79d9e3f162ed4c49477a329703610cdf5ddff54cec2cc4da62a . The series are synthetic, and I say exactly how they are generated below. The one thing that is not synthetic is the arithmetic, and it is the part that does the work. In short: w firing at ratio T cannot see any uniform daily growth below g = T^ 2/w - 1 . For w=7, T=1.20 that is diff -rq prints nothing, git diff would print nothing , the vendor quietly bumped the harness scaffold, the fleet dashboard's short-window detectors stayed silent the long window fired once, on day 58 — 46 days late , and the anchor blocked on day 12 with the growth attributed to vendor: +2600 B .On July 13, Dipankar Sarkar https://dev.to/alex spinov/comment/3b3eb replied to my post on usage logs that report zero tokens for delivered text https://finops.spinov.online/blog/delivered-but-unbilled-stream-gate/ with a sharp correction. His argument: a provider tokenizer bump and real suppression look identical if you only watch a threshold, so the disambiguator should be population, not threshold. "So the block condition isn't 'delta widened past X,' it's 'this stream's delta diverged from the fleet's,' per-stream anomaly against a rolling baseline of everyone else." He is right, and I said so. Then I argued the thing that became this article: a rolling baseline adapts, so anything that grows slower than the adaptation window never diverges from the fleet. It walks the fleet along with it. Boiling frog. What you need is a second anchor that is frozen at a known-good date, and for a solo developer with no fleet, a canary request with deterministic input is a population of one. And then I ended my own comment https://dev.to/alex spinov/comment/3b3hf with this: "To be straight about status, this is a design argument and not something I have measured. The re-tokenized floor I have run. The fleet-versus-canary discrimination I have not." That was two days ago. This post is me paying that off. The domain moved I am measuring input cost drift, not usage suppression but the structure of the question is the same one Dipankar and I were arguing about: can a baseline built from your own recent history see a thing that moves your own recent history? This part is not a discovery. It is a derivative, and it is worth doing on paper before you touch a fixture, because it tells you what the measurement is allowed to find. A rolling window compares today against a baseline built from the previous w days. That baseline is centred roughly w/2 days back. Under uniform daily growth g , today's level over the baseline's level is about 1+g ^ w/2 . The alarm fires when that ratio exceeds the threshold T . Solve for g : g = T^ 2/w - 1 Any growth slower than g is invisible forever . Not "for a while". The ratio the detector computes stops rising, because the baseline is climbing at the same rate as the signal. Here is what the tool prints before it has read a single byte of data: window threshold invisible below compounds to, over 30 days w=7 T=1.15 4.07%/day x3.31 w=7 T=1.20 5.35%/day x4.77 w=14 T=1.20 2.64%/day x2.18 w=28 T=1.20 1.31%/day x1.48 w=28 T=1.10 0.68%/day x1.23 Read the right-hand column. A seven-day window at a 20 percent threshold is blind to any drift that multiplies your input cost by 4.77x over a month . That is the prediction. The measurement is not the formula, it is the question of where real context creep lands relative to that band, and whether you can pick a T low enough to close it without the alarm screaming every Tuesday. Sixty days. Forty runs per day, fixed . I fixed the run count on purpose: variable traffic adds noise to a daily average, and noise is the thing that pushes the dashboard's threshold up. Removing it is a handicap in the dashboard's favour, and I would rather hand the dashboard its best case than be accused of rigging it. The floor at pin time is 61,060 bytes: | field | bytes | origin | |---|---|---| tools json | 26,000 | local file | system prompt | 12,000 | local file | instructions CLAUDE.md | 9,000 | local file | harness scaffold | 6,000 | vendor, not a local file | mcp a , mcp b | 4,000 each | local files | session id , timestamp Work per run is a lognormal draw, median 18,000 bytes, sigma 0.8, multiplied by a daily job-mix factor lognormal, sigma 0.15 . So a typical run is 86,033 bytes and the floor is 71.0 percent of it. Every world shares one noise realization and one anchor: only the floor trajectory differs. The dashboard is never handed a harder draw than the anchor gets. The six worlds: | world | what happens | |---|---| control | nothing changes | creep | one policy section appended to CLAUDE.md every day, a 3rd MCP server on day 14, a 4th on day 33, 12 tool schemas on day 40 | slowcreep | one policy section every third day, one MCP server on day 33 | spike | someone pastes a 40 KB architecture doc into CLAUDE.md on day 30 | harness | no local file changes at all. The vendor bumps its scaffold on days 12, 31 and 48 | workload | the floor never moves. Work per run doubles on day 30 | Each policy section is uniquely generated, 380 to 620 bytes, with its own heading and body. I mention that because my last cost article was dropped in review for a fixture that repeated one paragraph thirty times, which was a fair hit. Growth rates that come out of that: creep runs at +1.044 percent/day and reaches 112,700 bytes. slowcreep runs at +0.347 percent/day and reaches 74,885 bytes, up 22.6 percent. Nobody would notice either one by looking. Every rolling detector gets its own best deal: the tool walks a threshold ladder from 1.05 upward and picks the lowest threshold that raises zero false alarms on the flat world . That is the most generous calibration you can honestly give an alerting rule, because the first thing that happens to a noisy rule in real life is that somebody raises the threshold until it shuts up. Here is what came back, condensed from the real output: | world | rolling median w=7 | rolling mean w=7 | EWMA a=0.3 | rolling median w=28 | frozen anchor, tol 2% | |---|---|---|---|---|---| control | 0 alarms T=1.20 | 0 T=1.20 | 0 T=1.20 | 0 T=1.15 | PASS, all 60 days | creep 1.04%/d | 1, on day 40 | 1, day 40 | 1, day 40 | 15, from day 30 | BLOCK day 3 | slowcreep 0.35%/d | 0 | 0 | 0 | 0 | BLOCK day 9 | harness | 0 | 0 | 0 | 1, day 58 | BLOCK day 12 | spike | 4, day 30 | 2, day 30 | 2, day 30 | 15, day 30 | BLOCK day 30 | workload | 4, day 30 | 2, day 30 | 2, day 30 | 14, day 30 | PASS, all 60 days | Look at the creep row for the short windows. One alarm each, and it lands on day 40 , the day twelve tool schemas were installed. Not on the creep. On the step. The detector is doing exactly what it is built to do, which is notice a discontinuity, and the 51 KB the floor gained by walking there is invisible to it. Now the long window, w=28 at T=1.15 . The arithmetic said it is blind below 1.00 percent/day . creep runs at 1.044 percent/day, just above the line, and the detector fires 15 times starting on day 30. slowcreep runs at 0.347 percent/day, below the line, and it fires zero times in 60 days. The formula predicted both the hit and the miss before the data existed. That is the whole argument in two rows, and it is why I put the arithmetic first. The long window is not free, by the way. It paid for that sensitivity with 15 alarms on spike and 14 on workload . The harness world is the one I would put in front of a skeptic. Nothing in the repository changes. Not one byte. The demo runs diff -rq between a day-0 snapshot and a day-59 snapshot of every local file that feeds the request: diff -rq repo day000 repo day059: no differences. git diff would print nothing. sha256 of every local file, day 0 vs day 59: d0 063d5a8a0fe8267fc4093041701996f728662aab22b0763dd0225de767bff895 ./CLAUDE.md d0 e3b98852646c31827f303b3762410a3ca42f3a51c2a2201614e21b76fae51e70 ./mcp/a.json d0 07acba3d89f17bc9467bcbac3f2bcc72cfff4c07ad65694d86691a66a080f80c ./mcp/b.json d0 25cb751f2526dafd5e0f7273fe29a7abac797424f442cfff28ea82ca013bf8cf ./system prompt.txt d0 3dc0f40d1489590ef5a98c464c11378f97e4133aa1a6fcc984fa5d386525440c ./tools.json d59 063d5a8a0fe8267fc4093041701996f728662aab22b0763dd0225de767bff895 ./CLAUDE.md d59 e3b98852646c31827f303b3762410a3ca42f3a51c2a2201614e21b76fae51e70 ./mcp/a.json d59 07acba3d89f17bc9467bcbac3f2bcc72cfff4c07ad65694d86691a66a080f80c ./mcp/b.json d59 25cb751f2526dafd5e0f7273fe29a7abac797424f442cfff28ea82ca013bf8cf ./system prompt.txt d59 3dc0f40d1489590ef5a98c464c11378f97e4133aa1a6fcc984fa5d386525440c ./tools.json Meanwhile the vendor added 2,600 bytes of tool-call protocol preamble to every request on day 12, another 3,100 on day 31, another 3,400 on day 48. By day 59 the rendered request carries 14.9 percent more floor than it did at pin time, on every single call, forever. Three instruments looked at that world. git diff reported nothing. The rolling detectors on the usage log reported nothing the long window eventually woke up on day 58, which is 46 days late and only after the third bump . The anchor reported this, on day 12, from the same command: bash $ drift anchor gate.py check worlds/anchor.json worlds/harness/canary/day012.json anchor 61060 B pinned day 0 tolerance 2.0% today 63660 B day 12 drift +4.26% harness scaffold +2600 B vendor unchanged same sha256 : instructions, mcp a, mcp b, session id, system prompt, timestamp, tools json from local files: +0 B from the vendor: +2600 B verdict: BLOCK - exit 1 drift +4.26% is above the 2.0% tolerance exit=1 The same command, the same anchor, the same day, in the control world: bash $ drift anchor gate.py check worlds/anchor.json worlds/control/canary/day012.json today 61060 B day 12 drift +0.00% verdict: PASS - exit 0 drift +0.00% is within the 2.0% tolerance exit=0 That line from local files: +0 B from the vendor: +2600 B is the reason the anchor measures the rendered request and not the files you remembered to list. Your repo is not the thing you pay for. The rendered request is the thing you pay for. I would not trust this article if it did not have this section, so here it is. The workload world: the floor never changes, and on day 30 the work per run doubles retrieval starts returning twice the chunks, users paste bigger inputs, whatever . The rolling detector fires on day 30 and it is completely correct to do so. The anchor passes all 60 days , because the frozen canary task is not doing more work. Its input really did not drift. The anchor is blind to the entire class of cost growth that lives in the workload the context tax, where every step re-bills the whole transcript https://finops.spinov.online/blog/context-tax-measure-transcript-rebill/ , and that class is real and often bigger than the floor. Now put spike and workload side by side in that results table. Look at the rolling columns. Same detector, same day 30, near-identical alarm counts 4, 2, 2 on the short windows, 15 vs 14 on the long one . From inside the usage log those two worlds are indistinguishable . One of them is a 40 KB doc pasted into an instruction file that you will now pay for on every call until someone deletes it. The other is your product doing more work, which is what you wanted. The dashboard raises the identical alarm for both. The anchor tells them apart: BLOCK on the paste, PASS on the workload. That is not the anchor being more sensitive. It is the anchor answering a different, narrower question, and answering it cleanly. So the honest summary is a division of labour, not a replacement. Keep the rolling detector. It catches the paste, the step, and the workload change. Add the anchor. It catches the year. There is one parameter that can flip the whole result, so I swept it and I am publishing the sweep: how much of a typical request is floor? The noise in a fleet average comes from the work , not from the floor. The floor is the same on every call. So the less work your runs carry, the quieter your fleet average is, the lower a threshold you can run without false alarms, and the closer your dashboard gets to being a canary all by itself. Here is the sweep on the slowcreep world, with the w=7 detector recalibrated to its own zero-false-alarm threshold at each work level: work median floor share w=7 clean T blind below alarms on slowcreep first wm002000 95.8% T=1.05 1.40%/day 4 <- sees the creep 33 wm005000 89.8% T=1.05 1.40%/day 6 <- sees the creep 12 wm010000 81.8% T=1.15 4.07%/day 0 - wm018000 71.7% T=1.15 4.07%/day 0 - wm030000 59.2% T=2.00 21.90%/day 0 - wm060000 41.7% T=2.00 21.90%/day 0 - At a floor share around 90 percent and above, the dashboard sees the creep and this article's headline does not apply to you . The flip in my run happens between 81.8 percent blind and 89.8 percent sees . If your agent's requests are almost entirely system prompt and tool schemas with a tiny user turn on top, you already own a canary. You did not have to freeze anything. For everyone else, notice what happens at the bottom of that table. At a 42 percent floor share, the ladder had to climb to T=2.00 to stay quiet on a flat fleet. A threshold of 2.00 means "wake me when input cost doubles day over day", and its blind band is 21.9 percent a day. Nobody ships that alert. That is the mechanism in one line: the noisier your work, the higher your threshold must go, and the wider the door you leave open for drift. I swept the day-to-day noise too, sigma from 0.05 to 0.25. At sigma 0.05, a very quiet fleet, the w=7 detector does catch one alarm on slowcreep , on day 36 27 days after the anchor . At every other noise level: zero. The tool counts bytes. That is deliberate, and it is also the answer to the first objection I would raise if I were reading this. Drift is a ratio , and a ratio survives any linear rescale: a x2 / a x1 = x2 / x1 . Whatever your bytes-per-token constant is, whatever your price per token is, it cancels. So I do not need a tokenizer to say the floor grew 22.6 percent, and I refuse to multiply by a made-up constant to make the number look like money. Caching is the interesting version of this objection. A stable prefix is cacheable, and a cache read can cost about a tenth of a fresh write, so caching changes the level of what you pay by a large factor. It does not change the ratio, so it does not change the verdict. The floor still grew 22.6 percent, you are still paying the drift, and you are paying it on every call. There is a nastier detail underneath: editing an instruction file invalidates the prefix , so on the day someone appends a policy section you pay a full cache write on top the cache-break detector https://finops.spinov.online/blog/cache-break-detector/ is about exactly that failure . Slow creep is not one cheap event. It is a cheap event that also throws away your cache, repeatedly. If you want dollars, multiply. Just do not let the multiplication happen before the comparison. Two of my own posts are close enough to this one that I owe you a direct answer. Sliding-Window Spend Guard https://finops.spinov.online/blog/sliding-window-spend-guard/ also uses a window. The difference is where the threshold comes from: there, the cap is absolute "$X per window, refuse the next call" , and the window is only a way to aggregate. Here, the whole subject is a threshold derived from your own history , which is the thing that fails. An absolute cap is immune to this failure mode by construction. It is also the reason the window guard is a good design and a fleet-relative alert is a trap: one of them has a baseline that cannot be moved by the drift it is supposed to catch. MCP Tool Pin Verify https://finops.spinov.online/blog/mcp-tool-pin-verify/ also pins a hash. The difference is what the pin is for: there it protects the semantic integrity of a tool description against a malicious rug-pull. Here the pin protects a level , and the adversary is not an attacker, it is your own team, being helpful, one policy section at a time. Same mechanism, different failure. Pinning tool manifests does not tell you your floor grew, and this gate does not tell you a tool description turned hostile. Run both. Both belong to the same family: a pre-execution gate https://finops.spinov.online/blog/pre-execution-gate-for-ai-agents/ that decides before the money is spent instead of a chart that explains the money after. A cost gate has exactly one bug it must never have: reporting "all clear" because the artifact it was supposed to measure went missing. My previous gate had that bug a zero-byte baseline made a division produce 0.0 , which was below the threshold, which was a pass, on 2.25 million tokens . It was caught in review and the article was dropped. Fair. So this one has three exits and a rule: 0 PASS drift within tolerance, canary frozen, every declared field present and non-empty. 1 BLOCK drift above tolerance. 2 STRUCTURAL the input is unusable. There is no path where a missing, emptied or truncated artifact returns 0. A record that faithfully reports a genuinely smaller request is a PASS — that is the whole point of measuring level, not a fail-open. What cannot happen is the measurement vanishing, or a field dropping out, and being scored as "no drift." Nine degenerate inputs, all from the real run: --- dropped field --- STRUCTURAL: declared field missing from today's record: tools json STRUCTURAL: a bytes-only check would have called this -42.6% drift and passed it verdict: STRUCTURAL - exit 2 fail-closed: unusable input is never a pass exit=2 --- zero byte field --- STRUCTURAL: declared field rendered zero bytes: mcp b exit=2 --- unknown field --- STRUCTURAL: undeclared field in today's record: mcp c STRUCTURAL: you do not know everything your harness is sending; re-pin deliberately exit=2 --- task changed --- STRUCTURAL: canary task changed: task sha256 c81db92e6a9f... = pinned 7439b8b0addf... STRUCTURAL: a canary that drifts is not a canary exit=2 --- volatile grew --- STRUCTURAL: volatile field session id changed length: 36 B pinned, 96 B today STRUCTURAL: a volatile field with a moving length is not volatile, it is drift in disguise exit=2 Plus missing , empty , truncated , and inconsistent total the parts do not sum to the stated total . Nine cases, nine exit 2s. The dropped field line is the one to stare at: a naive bytes-only comparison reads that record as 42.6 percent cheaper than the anchor and passes it. That is the fail-open shape, and it is why the manifest is a manifest and not a number. And on the typical scenario, the one this tool exists for, it fails closed in the direction that costs someone a conversation: slowcreep day 8 passes at +1.80 percent, day 9 blocks at +2.79 percent, exit 1, attribution instructions +1706 B local:CLAUDE.md . Your CI goes red because somebody added four paragraphs of rules over nine days. That is the intended behaviour, and if it annoys you, re-pin the anchor. Deliberately. In a commit. With your name on it. It is 1,726 bytes and there is nothing clever in it: { "canary id": "floor-canary-v1", "pinned at": "day 0", "tolerance pct": 2.0, "task sha256": "7439b8b0addf5009c942bb2f1c42c0c4e3489b13990957f8a47253aaa429a8c8", "rendered bytes": 61060, "manifest": { "tools json": {"bytes": 26000, "sha256": "3dc0f40d...", "origin": "local:tools.json", "volatile": false}, "system prompt": {"bytes": 12000, "sha256": "25cb751f...", "origin": "local:system prompt.txt","volatile": false}, "instructions": {"bytes": 9000, "sha256": "063d5a8a...", "origin": "local:CLAUDE.md", "volatile": false}, "harness scaffold": {"bytes": 6000, "sha256": "39b235eb...", "origin": "vendor", "volatile": false}, "mcp a": {"bytes": 4000, "sha256": "e3b98852...", "origin": "local:mcp/a.json", "volatile": false}, "mcp b": {"bytes": 4000, "sha256": "07acba3d...", "origin": "local:mcp/b.json", "volatile": false}, "session id": {"bytes": 36, "sha256": "96311dd8...", "origin": "runtime", "volatile": true}, "timestamp": {"bytes": 24, "sha256": "2956a7fc...", "origin": "runtime", "volatile": true} } } Hashes truncated here for width. They are full-length in the file. One detail worth pausing on: instructions is pinned at 063d5a8a... , and that is the same digest shasum printed for CLAUDE.md on day 59 of the harness world. The file is provably untouched, and the request still grew. Today's record is the same shape: per-field byte counts and digests for the rendered request. Byte counts and hashes, not payloads, which is the shape a privacy-conscious request log already has. If your harness can dump the request it is about to send, you can produce this. If it cannot, that is a finding. Three files. All of it, because a reproducibility claim you cannot check is just a claim. drift anchor gate.py : bash /usr/bin/env python3 """drift anchor gate.py - offline, keyless, zero-network, read-only, stdlib-only Python 3.13 . One question: has the INPUT COST of a known-good run gone up since a known-good day? A rolling baseline median / mean / EWMA over your fleet answers that question by comparing today against your own recent history. It is a detector of SPEED. The invoice is charged on LEVEL. This tool answers the same question against a baseline that does not move: an anchor pinned on day 0 to a FROZEN canary task, so the only thing that can change between then and now is what your harness wraps around it. check one day against the anchor. Exit 0 PASS / 1 BLOCK / 2 STRUCTURAL. replay N days, anchor and rolling detectors side by side on the same log. sweep the two parameters that could flip the result: floor share, day noise. blindband pure arithmetic, no data: the growth rate a rolling window cannot see. Exit contract fail-closed : 0 PASS drift within tolerance, canary frozen, every declared field present and non-empty 1 BLOCK drift above tolerance 2 STRUCTURAL input unusable: unreadable, empty, truncated, a declared field missing or rendered zero bytes, an undeclared field present, the canary task changed, a volatile field that changed length, a record whose parts do not sum to its total. There is no path where a missing, emptied or truncated artifact returns 0, and a dropped or zeroed field fails closed too. A gate that answers "no drift detected" because the artifact vanished is worse than no gate. This tool never writes, never opens a socket, never imports anything outside the stdlib. """ import hashlib import json import os import statistics import sys LADDER = 1.05, 1.10, 1.15, 1.20, 1.25, 1.30, 1.40, 1.50, 2.00 FAMILIES = "rolling median w=7", "median", 7 , "rolling mean w=7", "mean", 7 , "EWMA alpha=0.3", "ewma", 7 , "rolling median w=28", "median", 28 ALPHA = 0.3 ---------------------------------------------------------------- the gate def load json path, what : if not os.path.exists path : die "%s not found: %s" % what, path if os.path.getsize path == 0: die "%s is zero bytes: %s" % what, path try: with open path as fh: return json.load fh except ValueError, OSError as exc: die "%s is not readable JSON: %s %s " % what, path, exc. class . name def die msg : print "STRUCTURAL: %s" % msg print "verdict: STRUCTURAL - exit 2 fail-closed: unusable input is never a pass " sys.exit 2 def check anchor, today, quiet=False : """Return verdict, drift pct, lines . verdict in PASS / BLOCK / STRUCTURAL.""" out = man = anchor "manifest" fields = today.get "fields" if not isinstance fields, dict or not fields: return "STRUCTURAL", 0.0, "no fields in today's record" if today.get "canary id" = anchor "canary id" : return "STRUCTURAL", 0.0, "canary id mismatch: %r vs pinned %r" % today.get "canary id" , anchor "canary id" if today.get "task sha256" = anchor "task sha256" : return "STRUCTURAL", 0.0, "canary task changed: task sha256 %s... = pinned %s..." % str today.get "task sha256" :12 , anchor "task sha256" :12 , "a canary that drifts is not a canary" missing = k for k in man if k not in fields unknown = k for k in fields if k not in man empty = k for k, v in fields.items if v.get "bytes", 0 <= 0 if missing: naive = sum v "bytes" for v in fields.values / anchor "rendered bytes" - 1 return "STRUCTURAL", 0.0, "declared field missing from today's record: %s" % ", ".join sorted missing , "a bytes-only check would have called this %+.1f%% drift and passed it" % naive 100 if unknown: return "STRUCTURAL", 0.0, "undeclared field in today's record: %s" % ", ".join sorted unknown , "you do not know everything your harness is sending; re-pin deliberately" if empty: return "STRUCTURAL", 0.0, "declared field rendered zero bytes: %s" % ", ".join sorted empty for k, v in man.items : if v.get "volatile" and fields k "bytes" = v "bytes" : return "STRUCTURAL", 0.0, "volatile field %s changed length: %d B pinned, %d B today" % k, v "bytes" , fields k "bytes" , "a volatile field with a moving length is not volatile, it is drift in disguise" total = sum v "bytes" for v in fields.values if total = today.get "rendered bytes" : return "STRUCTURAL", 0.0, "record does not sum: fields %d B, rendered bytes %d B" % total, today.get "rendered bytes" drift = total / anchor "rendered bytes" - 1 tol = anchor "tolerance pct" / 100.0 deltas = sorted fields k "bytes" - man k "bytes" , k for k in man , key=lambda t: -abs t 0 local = sum d for d, k in deltas if man k "origin" .startswith "local" vendor = sum d for d, k in deltas if not man k "origin" .startswith "local" if not quiet: out.append "anchor %8d B pinned %s tolerance %.1f%%" % anchor "rendered bytes" , anchor "pinned at" , anchor "tolerance pct" out.append "today %8d B day %-3s drift %+.2f%%" % total, today.get "day", "?" , drift 100 out.append "" for d, k in deltas: if d: out.append " %-18s %+8d B %s" % k, d, man k "origin" same = k for d, k in deltas if d == 0 and fields k "sha256" == man k "sha256" if same: out.append " unchanged same sha256 : %s" % ", ".join sorted same out.append "" out.append " from local files: %+d B from the vendor: %+d B" % local, vendor verdict = "BLOCK" if drift tol else "PASS" return verdict, drift 100, out def cmd check args : anchor = load json args 0 , "anchor" today = load json args 1 , "today's record" verdict, drift, lines = check anchor, today if verdict == "STRUCTURAL": for ln in lines: print "STRUCTURAL: %s" % ln print "verdict: STRUCTURAL - exit 2 fail-closed: unusable input is never a pass " return 2 body = "\n".join lines print body print "" if verdict == "BLOCK": print "verdict: BLOCK - exit 1 drift %+.2f%% is above the %.1f%% tolerance " % drift, anchor "tolerance pct" else: print "verdict: PASS - exit 0 drift %+.2f%% is within the %.1f%% tolerance " % drift, anchor "tolerance pct" print "report-sha256: %s" % hashlib.sha256 body.encode .hexdigest return 1 if verdict == "BLOCK" else 0 ---------------------------------------------------------- the dashboard def daily means path : if not os.path.exists path or os.path.getsize path == 0: die "usage log missing or empty: %s" % path days = {} with open path as fh: for line in fh: r = json.loads line days.setdefault r "day" , .append r "input bytes" if not days: die "usage log has no records: %s" % path return statistics.mean days d for d in sorted days def alarms series, kind, w, t : """Days on which today's mean exceeds the baseline built from the PREVIOUS w days by factor t.""" hits = ew = None for i, x in enumerate series : if i = w: if kind == "median": base = statistics.median series i - w:i elif kind == "mean": base = statistics.mean series i - w:i else: base = ew if base and x / base t: hits.append i if kind == "ewma": ew = x if ew is None else ALPHA x + 1 - ALPHA ew return hits def calibrate control, kind, w : """The lowest threshold on the ladder that raises zero false alarms on a flat world.""" for t in LADDER: if not alarms control, kind, w, t : return t return None def blind band t, w : """Uniform daily growth g is invisible while 1+g ^ w/2 <= T. Arithmetic, not a finding.""" return t 2.0 / w - 1.0 def cmd blindband args : print "blind band of a rolling window: it compares today against a baseline centred" print "about w/2 days back, so a uniform daily growth g only trips it when 1+g ^ w/2 T." print "solve for g: g = T^ 2/w - 1 any growth slower than g is invisible forever." print "" print " window threshold invisible below compounds to, over 30 days" for w, t in 7, 1.15 , 7, 1.20 , 14, 1.20 , 28, 1.20 , 28, 1.10 : g = blind band t, w print " w=%-4d T=%.2f %6.2f%%/day x%.2f" % w, t, g 100, 1 + g 30 print "" print "this is arithmetic. it is true before you collect a single byte of data." print "the measurement is where real context creep lands relative to that band." return 0 def cmd replay args : world, ctrl = args 0 , args 1 anchor = load json os.path.join os.path.dirname world , "anchor.json" , "anchor" events = load json os.path.join world, "events.json" , "events" series = daily means os.path.join world, "usage.jsonl" control = daily means os.path.join ctrl, "usage.jsonl" print "world: %s days: %d runs/day: fixed" % os.path.basename world , len series floor0 = anchor "rendered bytes" typical = statistics.mean control a flat fleet, so this is the work profile, not the drift print "floor at pin time %d B typical run %d B floor share %.1f%%" % floor0, round typical , 100.0 floor0 / typical print "" print " detector threshold alarms first on the day of" for name, kind, w in FAMILIES: t = calibrate control, kind, w if t is None: print " %-20s none clean on a flat world" % name continue hits = alarms series, kind, w, t first = hits 0 if hits else None ev = events.get str first , "nothing: this one is about the creep" if first is not None else "-" print " %-20s T=%.2f %2d %-5s %s" % name, t, len hits , first if first is not None else "-", ev g = blind band t, w print " %-20s blind below %.2f%%/day x%.2f over 30 days " % "", g 100, 1 + g 30 first block, first struct, drift at, final = None, None, 0.0, floor0 for day in range len series : rec = load json os.path.join world, "canary", "day%03d.json" % day , "record" verdict, drift, = check anchor, rec, quiet=True if verdict == "BLOCK" and first block is None: first block, drift at = day, drift if verdict == "STRUCTURAL" and first struct is None: first struct = day final = rec "rendered bytes" print "" if first block is None and first struct is None: print " frozen anchor tol=%.1f%% PASS on all %d days: the frozen input never drifted" % anchor "tolerance pct" , len series if first block is not None: print " frozen anchor tol=%.1f%% BLOCK on day %d drift %+.2f%% - exit 1" % anchor "tolerance pct" , first block, drift at if first struct is not None: print " frozen anchor tol=%.1f%% STRUCTURAL on day %d %s - exit 2" % anchor "tolerance pct" , first struct, events.get str first struct , "the set of fields changed" print " floor on the last day %d B %+.1f%% against the anchor " % final, final / floor0 - 1 100 return 1 if first block is not None else 2 if first struct is not None else 0 def cmd sweep args : root, label = args 0 .rstrip "/" , args 1 if not os.path.isdir root : die "sweep root not found: %s" % root anchor = load json os.path.join os.path.dirname root , "anchor.json" , "anchor" print " %-10s floor share w=7 clean T blind below alarms on slowcreep first" % label for name in sorted os.listdir root : d = os.path.join root, name ctrl = daily means os.path.join d, "control", "usage.jsonl" slow = daily means os.path.join d, "slowcreep", "usage.jsonl" t = calibrate ctrl, "median", 7 hits = alarms slow, "median", 7, t share = 100.0 anchor "rendered bytes" / statistics.mean ctrl g = blind band t, 7 100 print " %-10s %6.1f%% T=%.2f %6.2f%%/day %2d %-18s %s" % name, share, t, g, len hits , "<- sees the creep" if hits else "", hits 0 if hits else "-" print "" print " neither parameter reaches the frozen anchor. It reads a deterministic input, so it" print " carries no work and no day-to-day noise: floor share 100%%, tolerance %.1f%%, same" % anchor "tolerance pct" print " BLOCK day in every row above. The rows only move the DASHBOARD." return 0 USAGE = """usage: drift anchor gate.py check