DEV Community

Dakota Wu
Dakota Wu

Posted on

A Runtime Host Permit for Vibe-Coded Indie APIs

A solo founder can vibe-code a weekend backend and still keep the bill at zero. The handlers can be messy. The outbound host list cannot. In this lane, engineering is a runtime host permit that fails closed before any paid hostname is resolved.

Generated code is good at looking complete. It is bad at respecting a bootstrapped budget. A draft that compiles is not a draft that stays free. The difference is not taste. It is whether the process is allowed to resolve the public internet.

The leak that import pinning misses

Import scanners catch branded SDKs. They miss a one-line urllib.request.urlopen aimed at a vendor the model remembered from training data. They also miss background “health” pings the agent added while being helpful. Those calls do not need a new dependency. They need a name.

Name resolution is the cost event. After DNS succeeds, the bill is a matter of time. A permit file that wraps getaddrinfo is the cheapest in-process control a solo operator can keep. It is not a service mesh. It is a lock on the laptop.

Vibe-coded features can ship today under that lock. Calling the result a platform is the mistake. The permit is the engineering. The generated route handlers are the draft.

Permit classes for a zero-bill demo

Three classes are enough for a weekend ship. Extra classes become prompt fodder. Keep the table small enough to hash in CI.

Class Meaning Typical members Default
LOCAL Loopback only localhost, 127.0.0.1, ::1 allow
FREE_REMOTE One operator-chosen free host a single explicit hostname deny until listed
PAID Everything else * deny

LOCAL is the demo. FREE_REMOTE is optional. PAID is never an accident. A founder who needs a second remote has left the zero-bill lane and should say so in the permit diff, not in chat.

1. Write a boring permit file

Keep the file next to the app, not in a prompt. Models rewrite prompts. They are slower to rewrite a path that CI hashes. First match wins. The wildcard deny is required, not decorative.

# egress.permit
# format: ALLOW|DENY <host-or-ip>
# comments start with #
ALLOW localhost
ALLOW 127.0.0.1
ALLOW ::1
DENY *
Enter fullscreen mode Exit fullscreen mode

A founder who later opts into a free server adds one ALLOW line for that hostname. Nothing else changes. The wildcard deny stays last. Treat a growing file as an incident, not as product iteration.

Hash it so an agent cannot silently widen the net.

sha256sum egress.permit > egress.permit.sha256
git add egress.permit egress.permit.sha256
Enter fullscreen mode Exit fullscreen mode

2. Install a process-wide resolver guard

The guard below is a compact example, not a security product. It wraps socket.getaddrinfo and refuses unmatched hosts before a TCP handshake. Load it before the ASGI app. A late import is an open window.

# permit_guard.py
from __future__ import annotations

import ipaddress
import socket
from pathlib import Path

PERMIT_PATH = Path(__file__).with_name("egress.permit")
_DENIED = object()


class PermitDenied(RuntimeError):
    pass


def _parse(path: Path) -> list[tuple[str, str]]:
    rules: list[tuple[str, str]] = []
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        action, _, target = line.partition(" ")
        action = action.upper()
        target = target.strip().lower()
        if action not in {"ALLOW", "DENY"} or not target:
            raise ValueError(f"invalid permit line: {raw!r}")
        rules.append((action, target))
    if not rules or rules[-1] != ("DENY", "*"):
        raise ValueError("egress.permit must end with DENY *")
    return rules


RULES = _parse(PERMIT_PATH)


def _host_key(host: str) -> str:
    host = host.strip().lower().rstrip(".")
    if host.startswith("[") and host.endswith("]"):
        host = host[1:-1]
    return host


def decide(host: str) -> str:
    key = _host_key(host)
    try:
        ipaddress.ip_address(key)
        identities = {key}
    except ValueError:
        identities = {key}
    for action, target in RULES:
        if target == "*" or target in identities:
            return action
    return "DENY"


_orig_getaddrinfo = socket.getaddrinfo


def _guarded_getaddrinfo(host, port, *args, **kwargs):
    if host is None:
        return _orig_getaddrinfo(host, port, *args, **kwargs)
    if decide(str(host)) != "ALLOW":
        raise PermitDenied(f"egress denied for host={host!r} port={port!r}")
    return _orig_getaddrinfo(host, port, *args, **kwargs)


socket.getaddrinfo = _guarded_getaddrinfo
Enter fullscreen mode Exit fullscreen mode

The wrapper is English-simple on purpose. It does not log denied hosts to a remote sink. Logging is another egress path. Print locally or fail the test. Do not phone home about a deny.

3. Add a hostile test the agent cannot charm

The suite must call a public hostname the permit does not list. If the suite is green without that failure, the guard is not installed. A loopback-only happy path is a false calm.

# test_egress_permit.py
import permit_guard
import socket
import urllib.error
import urllib.request

import pytest


def test_loopback_still_resolves():
    infos = socket.getaddrinfo("127.0.0.1", 9, type=socket.SOCK_STREAM)
    assert infos


def test_public_host_is_denied():
    with pytest.raises(permit_guard.PermitDenied):
        socket.getaddrinfo("example.com", 443, type=socket.SOCK_STREAM)


def test_stdlib_http_cannot_bypass_the_resolver():
    with pytest.raises((permit_guard.PermitDenied, urllib.error.URLError)):
        urllib.request.urlopen("https://example.com/", timeout=2)
Enter fullscreen mode Exit fullscreen mode

Run it on every agent session, not once per release fantasy.

python -m pytest -q test_egress_permit.py
python -c "import permit_guard, hashlib; print(hashlib.sha256(open('egress.permit','rb').read()).hexdigest())"
Enter fullscreen mode Exit fullscreen mode

Pin the hash check next to pytest. A permit that drifted is a failed build. A model that “simplified” DENY * into a comment is a failed build.

4. Draft with a free model; list a free server only if it is real

A solo operator can draft handlers with a free coding model and still treat that model as untrusted for network policy. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source project with free model access and a free server option. Those are availability claims. They are not a default ALLOW rule.

If a free server is used, it belongs on one explicit ALLOW line under the operator’s control. The permit does not infer it from marketing copy. The model does not get a second remote “for retries.” CI should fail when egress.permit grows without a human diff. Indie operators who want that draft lane can try MonkeyCode against this permit workflow. The wildcard deny stays the last line either way.

The rest of the method does not depend on any vendor. Remove the product and the guard still compiles. That is the point of putting policy in a file the process loads first.

5. Ship with the guard in the process, not in the README

README policy is not policy. Export one entry point that imports the guard before the app. A tired 01:00 restart should not discover a second, unguarded script.

# Makefile
.PHONY: demo test freeze

demo:
    python -c "import permit_guard, app; app.main()"

test:
    python -m pytest -q test_egress_permit.py

freeze:
    sha256sum -c egress.permit.sha256
Enter fullscreen mode Exit fullscreen mode

Keep app.py boring. Bind the demo server to loopback. Refuse 0.0.0.0 unless the founder is doing that on purpose and has already left the laptop-only contract.

# app.py — labeled example, not a framework
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = b'{"ok": true, "lane": "local"}\n'
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

def main() -> None:
    httpd = HTTPServer(("127.0.0.1", 8080), Handler)
    httpd.serve_forever()
Enter fullscreen mode Exit fullscreen mode

Debugging a false deny without opening the net

False denies happen. IPv6 forms, trailing dots, and bracketed literals are the usual three. Fix the comparer, not the permit. Do not “temporarily” comment out DENY * to see if the demo boots. That is how a weekend API grows a bill.

Work the failure in this order.

  1. Print the raw host the stack passed into getaddrinfo. Use a local print, not a webhook.
  2. Normalize once in _host_key. Do not add fuzzy suffix matching. Fuzzy matching is how evil.localhost.paid-vendor.example sneaks through a careless endswith.
  3. If the app needs an IP literal, list that literal. Do not allow every IPv4 address because one health check used 127.0.0.1.
  4. Re-run the hostile test after every comparer change. A comparer fix that makes example.com resolve is a regression, not a cleanup.
# labeled debug helper — keep it off by default
DEBUG_EGRESS = False

def decide(host: str) -> str:
    key = _host_key(host)
    for action, target in RULES:
        if target == "*" or target == key:
            if DEBUG_EGRESS:
                print(f"egress {action} {key} via {target}")
            return action
    return "DENY"
Enter fullscreen mode Exit fullscreen mode

What this does not cover

The wrapper cannot see a subprocess that shells out to curl. It cannot see a C extension that opens its own sockets. It cannot see raw IPs unless those IPs are listed or denied. It is not a replacement for unshare, a host firewall, or an allowlist at the edge. It also does not prove the handlers are correct. It only proves the process was not allowed to resolve a stranger.

Do not use this approach for payment traffic, health data, or multi-tenant production. Do not use it as evidence that a vibe-coded service is engineered. The permit keeps a demo unbilled. It does not make the demo true.

Solo founders who need guaranteed uptime, questionnaires, or vendor SLAs should pick a paid stack on purpose. Skip the theater of a laptop permit. Everyone else can ship today, accept the limits, and leave the wildcard deny in place.

Top comments (0)