{"slug": "the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version", "title": "The Do-Not-Call List for Free AI Tiers: A Request-Level Policy You Can Version", "summary": "A developer published a request-level policy pattern for governing when free AI tiers can be used, packaging it as a versioned JSON policy file plus a stdlib-only Python classifier that runs before each model call. The classifier returns ALLOW, SANDBOX, or DENY based on data class, sink, reversibility, human-in-the-loop status, and agent tools, and every decision record carries a SHA-256 hash of the exact policy version that produced it. The approach argues that gating at the pipeline level misses the single risky request inside an otherwise green pipeline.", "body_md": "A weekly dependency bot opens a pull request against a release branch. A bot posts an AI-written summary of the diff into the PR thread. Nobody reviews that summary before merge. That is the whole incident, and it needed no attacker.\n\nThis is a composite of ordinary failure modes, not a customer report. It happens because teams gate at the pipeline level. The risky call is usually a single request inside a green pipeline.\n\nThis article is an anti-use field guide. It teaches where a free AI lane must not be the caller, and what to call instead.\n\nA paid tier and a free tier can run the same prompt. The difference is who owns the failure. Free access usually means fewer contractual guarantees, shared capacity, and terms you did not negotiate.\n\nSo the question is never \"is the model good enough\". The question is \"what is the blast radius when this specific call is wrong\". Classify the request, then pick the lane.\n\nAsk these for every request, not every service.\n\nA request that is public, reversible, and tool-free is a fine candidate for a free lane. A request that is internal, irreversible, and shell-capable is not.\n\nPolicies in prose drift. Policies in a file can be hashed, diffed, and pinned per release.\n\nStart with a small JSON policy. Keep it boring on purpose.\n\n```\n{\n  \"version\": 3,\n  \"hard_deny\": {\n    \"data_class\": [\"secret\", \"pii\"],\n    \"sink\": [\"signed_artifact\", \"prod_config\", \"money\"]\n  },\n  \"sandbox_only\": {\n    \"data_class\": [\"internal\"],\n    \"agent_tools\": [\"shell\", \"network\"]\n  },\n  \"allow\": {\n    \"task\": [\"summarize\", \"rename_suggestion\", \"changelog_draft\"]\n  }\n}\n```\n\nNote the shape. Deny rules are absolute. Sandbox rules are advisory and require human review. The allow list is explicit, so unknown tasks fall to review instead of shipping.\n\nThis runs before the model call. It is stdlib-only and short enough to read in a review.\n\n``` python\n# classify.py — Python 3.11+, no third-party deps\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nimport sys\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\n\nclass Decision(str, Enum):\n    ALLOW = \"ALLOW\"\n    SANDBOX = \"SANDBOX\"\n    DENY = \"DENY\"\n\n@dataclass(frozen=True)\nclass Request:\n    task: str\n    data_class: str          # public | internal | pii | secret\n    sink: str                # draft | comment | signed_artifact | prod_config | money\n    reversible: bool\n    human_in_loop: bool\n    agent_tools: tuple[str, ...] = ()\n\ndef load_policy(path: Path) -> tuple[dict, str]:\n    raw = path.read_bytes()\n    return json.loads(raw), hashlib.sha256(raw).hexdigest()[:12]\n\ndef classify(req: Request, policy: dict) -> tuple[Decision, list[str]]:\n    reasons: list[str] = []\n\n    for name, blocked in policy[\"hard_deny\"].items():\n        if getattr(req, name) in blocked:\n            reasons.append(f\"hard_deny:{name}={getattr(req, name)}\")\n    if reasons:\n        return Decision.DENY, reasons\n\n    for name, gated in policy[\"sandbox_only\"].items():\n        value = getattr(req, name)\n        hit = any(v in value for v in gated) if isinstance(value, tuple) else value in gated\n        if hit:\n            reasons.append(f\"sandbox_only:{name}={value}\")\n\n    if req.task not in policy[\"allow\"][\"task\"]:\n        reasons.append(f\"task_not_allowlisted:{req.task}\")\n\n    if not req.reversible and not req.human_in_loop:\n        reasons.append(\"irreversible_without_review\")\n\n    return (Decision.SANDBOX if reasons else Decision.ALLOW), reasons or [\"clean\"]\n```\n\nThe policy hash is the important line. Every decision record carries the exact policy version that produced it.\n\nThe log is JSONL. One line per request. It is the artifact you will actually use during an incident.\n\n``` python\ndef emit(req: Request, decision: Decision, reasons: list[str], policy_hash: str, out=sys.stdout) -> None:\n    record = {\n        \"policy\": policy_hash,\n        \"decision\": decision.value,\n        \"reasons\": reasons,\n        **{k: list(v) if isinstance(v, tuple) else v for k, v in req.__dict__.items()},\n    }\n    out.write(json.dumps(record) + \"\\n\")\n```\n\nRun it against a batch and read the distribution before you trust it.\n\n```\npython classify.py policy.json < requests.jsonl > decisions.jsonl\njq -r 'select(.decision != \"ALLOW\") | .reasons[]' decisions.jsonl \\\n  | sort | uniq -c | sort -rn | head\n```\n\nThat second command is the debugging workflow. It tells you which rule fires most. If `task_not_allowlisted` dominates, your policy is too narrow. If `hard_deny:sink=signed_artifact` fires often, you have a design problem upstream.\n\nA classifier without tests is a comment. Parametrize the matrix and run it in CI.\n\n``` python\n# test_classify.py\nimport pytest\nfrom classify import Decision, Request, classify\n\nPOLICY = {\n    \"version\": 3,\n    \"hard_deny\": {\"data_class\": [\"secret\", \"pii\"],\n                  \"sink\": [\"signed_artifact\", \"prod_config\", \"money\"]},\n    \"sandbox_only\": {\"data_class\": [\"internal\"], \"agent_tools\": [\"shell\", \"network\"]},\n    \"allow\": {\"task\": [\"summarize\", \"rename_suggestion\", \"changelog_draft\"]},\n}\n\nCASES = [\n    (Request(\"summarize\", \"public\", \"draft\", True, False), Decision.ALLOW),\n    (Request(\"summarize\", \"pii\", \"draft\", True, True), Decision.DENY),\n    (Request(\"summarize\", \"public\", \"prod_config\", True, True), Decision.DENY),\n    (Request(\"summarize\", \"internal\", \"comment\", True, True), Decision.SANDBOX),\n    (Request(\"deploy\", \"public\", \"draft\", False, False, (\"shell\",)), Decision.SANDBOX),\n]\n\n@pytest.mark.parametrize(\"req,expected\", CASES)\ndef test_matrix(req, expected):\n    assert classify(req, POLICY)[0] is expected\n```\n\nThe fifth case is the one that saves you. A capable agent on an unlisted task lands in review, not in production.\n\nMost free-lane failures have a cheaper replacement. Reach for that first.\n\n| Red flag | Why the free lane is wrong | Better alternative | \n|---|---|---|\n| Output signs or approves anything | No accountability trail on a shared tier | Deterministic signer, human approver | \n| Input carries PII or secrets | Terms and retention are not yours | Redaction step, then classify again | \n| Caller holds shell or network tools | Irreversible side effects, no rollback | Sandbox with no credentials | \n| Same prompt runs on every commit | Cost and drift scale silently | Cached fixture, golden-file test | \n| Output lands in a config or migration | One bad token is an outage | Schema validation, then review | \n| Task is formatting or renaming | Deterministic problem, probabilistic tool | Formatter, AST rewrite, linter | \n| Latency sits on a user request path | Shared capacity, unknown queueing | Precompute, or a committed tier | \n\nPrint that table. Tape it to the review checklist. The first three rows are hard stops.\n\nWrite these before adoption, not after the first incident. Trigger any one and move the workload.\n\nCriterion five is the quiet one. A sandbox nobody reads is a production lane with extra steps.\n\nMonkeyCode ships as an open-source project with a free model allowance and a free server option. Those are operator-stated availability claims.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach.\n\nTreat both as shared capacity, not as entitlements. Quotas, limits, and terms change, so read the project's current documentation instead of trusting any blog post, including this one.\n\nIn this policy, that lane maps to `task in allow`, `data_class == public`, `sink == draft`, and `agent_tools == ()`. Summarize a public doc. Draft a changelog entry a human will edit. Suggest a test name. Keep the classifier in front of it, and the free server stays a staging surface rather than a signing surface.\n\nIf you want to test the workflow, start with the summarizer task. Leave the deploy bot out of it.\n\nThis classifier is a policy check, not an enforcement boundary. A caller can pass wrong labels. Pair it with egress rules, scoped credentials, and separate identities per lane.\n\nSkip this approach in three cases:\n\nThe pattern is small on purpose. Copy the two files, replace the allow list with your real tasks, and run the test matrix once. If more than half the cases land in `DENY`, your pipeline shape is the problem, and no tier will fix it.", "url": "https://wpnews.pro/news/the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version", "canonical_source": "https://dev.to/hackjs_8688/the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version-5cf", "published_at": "2026-10-17 05:56:34+00:00", "updated_at": "2026-10-17 06:00:41.551475+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "ai-tools", "developer-tools"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version", "markdown": "https://wpnews.pro/news/the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version.md", "text": "https://wpnews.pro/news/the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version.txt", "jsonld": "https://wpnews.pro/news/the-do-not-call-list-for-free-ai-tiers-a-request-level-policy-you-can-version.jsonld"}}