免費方案 AI 專案的證據會過期:以 TTL 建立失敗時預設阻擋的就緒檢查清單
Evidence Expires: A Fail-Closed Readiness Checklist with TTLs for Free-Tier AI Builds
這份檢查清單為免費方案 AI 專案提出以 TTL 管理就緒證據的 fail-closed 流程,過期、缺失、無法讀取或未通過的證據會阻止版本晉級。文中示例為 model_pin 設 7 天 TTL、quota_headroom 設 24 小時 TTL,並提供 YAML/JSON 記錄格式及 Python 檢查器,以退出碼區分通過、需重跑和阻擋。
A gate that passed last month is not a gate. It is a receipt for a system that no longer exists.
That is the failure mode this checklist targets. You wire up a readiness gate, it goes green, and three weeks later you promote a build using evidence that describes a runtime, a model default, or a quota policy that has already changed. The gate never lied. It just never expired.
What follows is a copy-paste workflow built on evidence TTLs. Every gate artifact carries an issue time and an expiry. Anything missing, unreadable, or stale blocks promotion by default.
Why a clean pass is only a snapshot
Model progress is currently outrunning release process. That gap shows up in the boring places, not in benchmarks.
- Model defaults drift. A provider re-points an alias, and your "verified" output shape changes.
- Free-tier policy drifts. Rate limits, allowed regions, and idle shutdown rules are not contracts.
- Host images drift. A base image gets a patch, and your dependency set is no longer the one you tested.
- Quota headroom drifts. The allowance you measured last week is not the allowance you have now.
- Reviewers drift. The person who signed off has context you cannot reconstruct.
None of these events break your code. They invalidate your evidence. A readiness gate that cannot detect that is decoration.
Three outcomes, and only three
Keep the verdict vocabulary tiny so nobody has to interpret it during an incident.
- PASS — fresh evidence exists, it says PASS, and it is inside its TTL.
- ESCALATE — evidence exists but is expired or partially complete. A human must re-run the gate and re-issue the record.
- BLOCK — evidence is missing, unreadable, negative, or future-dated. Promotion stops with no override path in the script.
Fail-closed means the default branch is BLOCK. You do not get a PASS because a file exists. You get a PASS because a file is current.
Assign a TTL to every artifact
TTLs are policy choices, not proofs. Pick them from how fast the underlying fact can change, then write them down where reviewers can argue with them.
| Gate | What the evidence proves | Starting TTL | On expiry |
|---|---|---|---|
model_pin |
Which model default your output contract was tested against | 7 days | Re-run prompts, re-issue record |
server_image |
Base image digest and package set | 14 days | Rebuild, re-run smoke test |
quota_headroom |
Current free allowance still fits peak usage | 24 hours | Re-measure before deploy window |
egress_review |
Which hosts and credentials the build may reach | 30 days | Re-review, re-sign |
rollback_drill |
You can return to the previous version | 30 days | Rehearse, attach log digest |
cost_ceiling |
Expected spend, including non-free overflow | 7 days | Re-estimate, re-approve |
If a row has no owner, delete the row. An unattributed gate becomes nobody's job within two sprints.
The checker: fail closed on stale evidence
Two files. One declares the gates and their TTLs; the other reads the evidence records and returns an exit code your CI can branch on.
Gate declarations
# gates.yaml
version: 1
gates:
model_pin:
evidence: .evidence/model_pin.json
ttl_hours: 168
owner: "@platform"
server_image:
evidence: .evidence/server_image.json
ttl_hours: 336
owner: "@platform"
quota_headroom:
evidence: .evidence/quota_headroom.json
ttl_hours: 24
owner: "@platform"
rollback_drill:
evidence: .evidence/rollback_drill.json
ttl_hours: 720
owner: "@oncall"
Evidence record format
The record is deliberately dumb: a timestamp, a result, the exact command that produced it, and a digest of the artifact. Do not store prose here; store the command.
{
"gate": "model_pin",
"issued_at": "2026-10-16T09:00:00Z",
"issued_by": "platform",
"command": "<the exact command that produced this record>",
"digest": "sha256:REPLACE_ME",
"result": "PASS"
}
The 60-line checker
This is a sketch, not a battle-tested tool. Confirm the paths and timestamp formats against your own repo before you wire it into a pipeline.
#!/usr/bin/env python3
"""Fail-closed gate checker.
Exit codes:
0 = all gates PASS
2 = at least one gate ESCALATE (stale, human re-run required)
1 = at least one gate BLOCK (missing, unreadable, negative)
"""
import datetime as dt
import json
import sys
from pathlib import Path
import yaml # pip install pyyaml
ROOT = Path(__file__).resolve().parent
ORDER = {"BLOCK": 0, "ESCALATE": 1, "PASS": 2}
def now_utc():
return dt.datetime.now(dt.timezone.utc)
def parse_ts(value):
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
def evaluate(name, spec, clock):
path = ROOT / spec["evidence"]
if not path.exists():
return "BLOCK", f"{name}: no evidence file at {spec['evidence']}"
try:
record = json.loads(path.read_text())
except (json.JSONDecodeError, OSError) as exc:
return "BLOCK", f"{name}: unreadable evidence ({exc})"
if record.get("result") != "PASS":
return "BLOCK", f"{name}: last result was {record.get('result')!r}"
if not record.get("command"):
return "BLOCK", f"{name}: record has no reproducible command"
try:
age_h = (clock - parse_ts(record["issued_at"])).total_seconds() / 3600
except (KeyError, ValueError) as exc:
return "BLOCK", f"{name}: bad issued_at ({exc})"
if age_h < 0:
return "BLOCK", f"{name}: evidence is dated in the future"
ttl = float(spec["ttl_hours"])
if age_h > ttl:
return "ESCALATE", f"{name}: {age_h:.1f}h old, ttl {ttl:.0f}h"
return "PASS", f"{name}: fresh ({age_h:.1f}h / {ttl:.0f}h), owner {spec.get('owner')}"
def main():
config = yaml.safe_load((ROOT / "gates.yaml").read_text())
clock = now_utc()
verdicts = [evaluate(n, s, clock) for n, s in config["gates"].items()]
for verdict, message in sorted(verdicts, key=lambda v: ORDER[v[0]]):
print(f"[{verdict:8}] {message}")
if any(v == "BLOCK" for v, _ in verdicts):
return 1
if any(v == "ESCALATE" for v, _ in verdicts):
return 2
return 0
if __name__ == "__main__":
sys.exit(main())
Wiring it in
python check_gates.py
code=$?
case "$code" in
0) echo "promotion allowed" ;;
2) echo "re-run stale gates, then re-check" ; exit 2 ;;
1) echo "do not promote" ; exit 1 ;;
esac
The important detail is the middle branch. Exit code 2 is not a failure of your software; it is a signal that a fact you depend on has aged out. Treat re-running as the default path, not as an exception.
Re-verification is a scheduling problem
TTLs fail in practice for one reason: re-running the gates costs money or attention, so teams stretch the TTL until the gate is meaningless.
The lever that makes the loop survivable is cheap execution. This is where MonkeyCode is relevant to the workflow: the project is open source, and it publishes a free model allowance plus a free server option. Those two make a scheduled re-verify job affordable in a way that a metered runner often is not — you can re-run a prompt-contract check, capture the evidence record, and let the TTL reset without opening a budget conversation. Confirm the current terms yourself before you depend on them; free allowances are advertised figures and they change. At the time of writing the operator advertises a 10M-token grant, which is a number to verify, not a number to bake into an SLA.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
A practical division of labor:
- The free server option runs the scheduled re-verify job and writes the evidence record.
- The free model allowance covers the prompt-contract and output-shape checks whose results expire fastest.
- Your own repo stays the source of truth. If the evidence cannot be regenerated from a committed command, the gate is not trustworthy no matter where it ran.
The copy-paste checklist
Run this before every promotion. Each item is a gate, and each gate must fail closed.
- Does every gate in
gates.yamlhave an owner and a TTL? - Does every evidence file exist, parse, and carry
result: PASS? - Does every record include the exact command that produced it?
- Is every record inside its TTL, and none future-dated?
- Can a second person reproduce one random record from scratch?
- Is the quota headroom check less than 24 hours old?
- Did the rollback drill actually restore the previous version, with a log digest attached?
- Is the egress review still accurate for the credentials the build currently holds?
- Does the promotion job block when the checker exits non-zero?
- Is there a named human who handles the ESCALATE branch within one working day?
Item 9 is the one teams skip. A checker that prints warnings but returns success is a dashboard, not a gate.
Limitations, and who should not use this
- This is not runtime monitoring. TTLs catch drift between deploys; they do not detect a provider failing mid-request. You still need health checks and alerting.
- TTLs are policy, not evidence. A seven-day TTL does not mean the fact was true for seven days — only that it was checked once within that window.
- Records are only as good as their commands. Unreproducible evidence should be deleted, not grandfathered.
- Free tiers carry no guarantee. Do not place regulated data, irreplaceable state, or an availability commitment on a free server option. Keep a documented path to paid or self-hosted infrastructure.
Skip this approach if your deployments are fully pinned and immutable with no third-party drift — the TTLs will just add ceremony. Skip it too if you have nowhere durable to store evidence, because expired records you cannot find are worse than no records.
The one habit that matters
Start with a single gate and a real TTL. Pick model_pin, give it seven days, and write the command that produces its evidence. When it goes stale, you will feel the friction immediately — and that friction is the point, because it is the moment you would otherwise have shipped on a fact that had already stopped being true.
If you want to try the loop without a budget, the open-source project's free server option is a reasonable place to run the first scheduled re-verify job.
來源:DEV Community · dev.to