Contents

AI Agentic Development Changes Who Builds Software — and That's an Infrastructure Problem

Table of Contents

  • The Shift Is Already Happening
  • The Problem Nobody Prepared For
  • The Design Principle: Safe by Default
  • The Platform Contract — What Every App Must Be
    • Supported Languages and Base Images
    • Supported Components
    • T-shirt Sizing
    • Port Contract
    • Health Endpoints
    • Secrets: Sealed, Always
    • Images: Commit SHA, Never Latest
    • Network: Default Deny, Every Time
  • The Three-Tier Monitoring Contract
    • System Tier — Automatic
    • Framework Tier — App Metrics
    • Business Tier — What the App Actually Does
  • The Review Gate — 35+ Checks Before Deploy
  • The Helm Chart — Five Questions, Full Platform
  • The CI/CD Pipeline — AI App Meets GitOps
  • The Skill Pipeline — AI Onboarding an AI App
  • The RACI Collapse
  • Conclusion

Here we are. Somewhere in the last twelve months, something quietly changed.

The Shift Is Already Happening

People who have never written a line of code in their professional lives are now producing working software. Not prototypes in the “I dragged boxes around in a no-code tool” sense. Real applications: Python backends with data models and business logic, React frontends that talk to APIs, database schemas that reflect years of domain knowledge no engineer could have extracted through interviews alone.

They built these with Claude, Cursor, Copilot. They described what they wanted in plain language. The agent coded it. They reviewed the output the way they review a Word document — checking the content, not the syntax.

This is not hype. I’ve watched it happen.

The question the industry keeps asking is “will AI replace developers?” That’s the wrong question, and it’s wasting time. The right question is: when non-technical people can build software, what are the operational consequences?

Because there are operational consequences. Real ones.


The Problem Nobody Prepared For

Run an AI coding bootcamp. Give a group of non-technical employees — analysts, brand managers, merchandisers, finance people — access to Claude or Cursor for a week. Ask them to build something that solves a real problem in their job.

They will build things. Things that actually run.

Then ask: where do you put them?

The options are obvious, and they are all wrong.

Their laptop. The app lives. Nobody else can use it. It dies when the laptop sleeps.

A shared development server. Everyone is root. One person’s experiment kills everyone else’s. Data from different projects bleeds together. There is no deployment process — it’s scp and nohup. Eventually someone pastes real data into a seed script and you have a security incident.

Production. Naaaa. A vibe-coded app built by someone who doesn’t know what a liveness probe is, whose “error handling” is whatever Claude decided to generate, running in production serving real users and touching real data. The support cost alone would be catastrophic.

None of these work. And if you give people the ability to build software without giving them a safe place to run it, you have created a problem, not solved one.

Vibe-Env is the answer: a purpose-built Kubernetes environment where vibe-coded apps can be deployed, tested, and shown to stakeholders — with every guardrail enforced by infrastructure, not documentation.


The Design Principle: Safe by Default

“Safe” gets applied to a lot of things in tech that are not safe. So let me be specific.

Every protection in this platform is enforced by infrastructure, not left to the app creator to implement correctly. A non-technical user cannot be expected to configure NetworkPolicies, know that latest image tags are mutable, understand why plain Kubernetes Secrets in Git are readable by anyone with repo access, or choose non-root containers.

The platform does all of this automatically. Where it cannot be automated, it blocks deployment if the condition is not met. The Helm chart validates required fields and fails loudly. The CI pipeline blocks on secrets and critical CVEs. The review step runs 35+ checks. The cluster blocks internet egress entirely.

Safe by default. No exceptions.

graph LR VPN -->|HTTPS| Traefik Traefik --> app-A Traefik --> app-B Traefik --> app-N app-A -.->|metrics + logs| Observability app-B -.->|metrics + logs| Observability app-N -.->|metrics + logs| Observability Observability --> Grafana Internet -->|blocked| Egress-Wall

The Platform Contract — What Every App Must Be

The contract is the center of the platform. It is not a style guide or a recommendation. It is a binary gate: the app meets it, or it does not deploy.

Supported Languages and Base Images

All base images come from an internal Harbor mirror. Docker Hub is blocked at cluster egress. The approved base images are Debian-based — never Alpine. musl libc compatibility issues with native extensions and certain runtimes cause subtle runtime failures that non-technical creators cannot debug.

LanguageRuntimeBase Image (Harbor mirror)Notes
TypeScript / JavaScriptNode.js 22mirror/node:22-slimDefault for new apps
TypeScript / JavaScriptNode.js 20mirror/node:20-slimLTS — use if locked
PythonPython 3.12mirror/python:3.12-slimDebian-slim
PythonPython 3.11mirror/python:3.11-slim
GoGo 1.23mirror/golang:1.23-bookworm (build) + mirror/gcr.io/distroless/static-debian12 (run)Multi-stage mandatory
Java / Kotlin / ScalaJVM 21mirror/eclipse-temurin:21-jre-jammyUbuntu-jammy
Java / Kotlin / ScalaJVM 17mirror/eclipse-temurin:17-jre-jammyLTS
RustRust 1.78mirror/rust:1.78-slim-bookworm (build) + mirror/gcr.io/distroless/static-debian12 (run)Multi-stage
PHPPHP 8.3mirror/php:8.3-fpm-bookworm

Multi-stage Dockerfile examples per runtime:

Node.js (TypeScript)

FROM mirror/node:22-slim AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm install
COPY src ./src
RUN npm run build

FROM mirror/node:22-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/* \
    && groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY --from=builder /app/dist ./dist
USER appuser
EXPOSE 8081 8082
CMD ["npm", "start"]

Python

FROM mirror/python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
    && groupadd -r appuser && useradd -r -g appuser appuser
COPY . .
USER appuser
EXPOSE 8081 8082
CMD ["sh", "-c", "python mgmt_server.py & uvicorn app.main:app --host 0.0.0.0 --port 8081"]

Go (multi-stage, distroless runtime)

FROM mirror/golang:1.23-bookworm AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .

FROM mirror/gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8081 8082
CMD ["/server"]

If a runtime is not in the table, it must be mirrored to Harbor before use. The creator opens a request to Platform; Platform mirrors it; Platform provides the Harbor path. The creator never pulls from the internet.

Supported Components

Every component a vibe app can use is pre-defined, pre-sized, and pre-wired:

ComponentImage (Harbor mirror)Purpose
appcreator-built, pushed to Harbormain application
nginxmirror/nginx:1.27reverse proxy, static files, SPA fallback
postgresmirror/postgres:16relational database
mongomirror/mongo:7.0document database
redismirror/redis:7.4cache, sessions
rabbitmqmirror/rabbitmq:3.13-managementmessage broker
postgres-exportermirror/prometheuscommunity/postgres-exporter:v0.15.0Prometheus metrics for PostgreSQL
mongodb-exportermirror/percona/mongodb_exporter:0.40Prometheus metrics for MongoDB
redis-exportermirror/oliver006/redis_exporter:v1.63.0Prometheus metrics for Redis

All in the same namespace. All wired together by the Helm chart. The creator chooses which ones they need; the platform wires the rest.

T-shirt Sizing

Resource limits are not optional. Every container must declare requests and limits. The platform provides five standard sizes:

SizeCPU requestCPU limitMemory requestMemory limitUse case
xs50m200m64Mi128Mistatic sites, minimal APIs
sm100m300m128Mi256Milight web apps
md200m500m256Mi512Mistandard apps (default)
lg500m1000m512Mi1Gidata-heavy apps
xl1000m2000m1Gi2Giintensive workloads

DB components always get md minimum, regardless of app size.

The non-technical creator does not set CPU and memory values in YAML. They pick a t-shirt size. The Helm chart translates it:

# From values.yaml — the creator sees this
app:
  size: md      # xs / sm / md / lg / xl

# What the Helm template renders into the Deployment
resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

Port Contract

Two ports, hard requirements:

PortNamePurposeExposed externally?
8081httpApplication traffic — API, frontend, everything the user interacts withYes — via Ingress
8082mgmtHealth probes, metrics — cluster-internal onlyNever

Port 8082 is never reachable from outside the cluster. The review gate (check B6) fails deployment if the Ingress routes traffic to port 8082.

Health Endpoints

Every long-running app must expose four endpoints on port 8082. The port is management-only and never routed externally.

EndpointResponse 200Response 503
GET /readiness{"status":"UP"}{"status":"OUT_OF_SERVICE"}
GET /liveness{"status":"UP"}{"status":"DOWN"}
GET /info{"app":"slug","version":"sha"}
GET /shutdownnull

The platform adds readinessProbe and livenessProbe to every Deployment automatically, pointing at port 8082. If the app can’t implement these endpoints, the platform provides an nginx sidecar that responds to them without touching the app code:

# values.yaml — enable the mgmt sidecar if app can't implement health endpoints
app:
  mgmtSidecar:
    enabled: true

The sidecar nginx config is auto-generated:

# nginx-mgmt-sidecar ConfigMap (auto-generated by Helm)
server {
  listen 8082;
  location /readiness { return 200 '{"status":"UP"}'; add_header Content-Type application/json; }
  location /liveness  { return 200 '{"status":"UP"}'; add_header Content-Type application/json; }
  location /shutdown  { return 200 'null'; add_header Content-Type application/json; }
  location /info      { return 200 '{"git":{"branch":"unknown"}}'; add_header Content-Type application/json; }
}

Secrets: Sealed, Always

Plain Kubernetes Secrets in Git are not secret. Anyone with repository read access can base64 -d them. This is a well-known problem and it still shows up in incident reports every month.

Vibe-Env uses Bitnami Sealed Secrets. The full flow — from plaintext to running pod — never exposes the secret in Git or in the pod spec:

flowchart LR A["plaintext credential"] -->|kubeseal| B["SealedSecret in Git"] B -->|ArgoCD sync| C["Sealed Secrets controller"] C -->|decrypt| D["Kubernetes Secret"] D -->|secretKeyRef| E["App Pod env var"]

The sealing workflow:

# 1. Create a plain Secret — locally, NEVER commit this file
kubectl create secret generic my-app-secret \
  --from-literal=DATABASE_URL="postgres://user:pass@my-app-postgres:5432/appdb" \
  --from-literal=API_KEY="sk-..." \
  --dry-run=client -o yaml > /tmp/secret-plain.yaml

# 2. Seal with cluster public key — output is safe to commit
kubeseal \
  --cert cluster.pub \
  --namespace platform-vibe-my-app \
  --format yaml \
  < /tmp/secret-plain.yaml > k8s/02-sealed-secrets.yaml

# 3. Delete the plaintext immediately
rm /tmp/secret-plain.yaml

# 4. Commit the sealed version — ArgoCD syncs it
git add k8s/02-sealed-secrets.yaml && git commit -m "chore: add app secret"

The encrypted SealedSecret is namespace-scoped and cluster-scoped — it cannot be decrypted in a different namespace or on a different cluster. Safe to commit to GitHub.

The Deployment template injects secrets only via secretKeyRef. No plaintext appears anywhere in the pod spec:

env:
  - name: DATABASE_URL
    valueFrom:
      secretKeyRef:
        name: my-app-secret
        key: DATABASE_URL
  - name: API_KEY
    valueFrom:
      secretKeyRef:
        name: my-app-secret
        key: API_KEY

E1 in the review gate: a plain Secret kind found in Git is a CRITICAL violation. Deployment is blocked and a security incident is raised.

Images: Commit SHA, Never Latest

image: harbor.internal.example.com/my-app:a3f91bc   # correct — immutable
image: harbor.internal.example.com/my-app:latest    # CRITICAL violation — deploy blocked

latest is a mutable tag. Between deploy and the next pod restart, a new push can silently replace what the tag points to. In an environment maintained by someone without an on-call rotation, a silent image change is the kind of thing that causes a Friday afternoon incident that nobody looks at until Monday.

The Helm template enforces this at render time:

image: {{ .Values.cluster.registry }}/{{ .Values.app.image.repository }}:{{
  .Values.app.image.tag | required "app.image.tag is required — use commit SHA, never 'latest'"
}}

Network: Default Deny, Every Time

Every namespace has the same runtime anatomy: nginx reverse proxy in front, app container on two ports (8081 for traffic, 8082 for management only), stateful components only reachable via explicit NetworkPolicy, and the observability stack cross-namespace but allowed only for metrics scrape and log shipping.

flowchart LR VPN -->|HTTPS| Traefik Traefik -->|port 80| nginx nginx -->|port 8081| app app -->|port 5432| postgres app -->|port 6379| redis postgres --> postgres-exporter postgres-exporter -.->|scrape| VictoriaMetrics app -.->|metrics port 8081| VictoriaMetrics app -.->|stdout JSON| Loki app -.->|traces| OTEL

First manifest applied to every namespace, before anything else:

# 01-network-policies.yaml — applied FIRST, mandatory
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: platform-vibe-my-app
spec:
  podSelector: {}
  policyTypes:
    - Ingress

Every allowed path requires an explicit policy. Five standard policies are generated by the Helm chart for a typical app with PostgreSQL:

---
# Traefik (kube-system) → app:8081
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-controller
  namespace: platform-vibe-my-app
spec:
  podSelector:
    matchLabels:
      app: my-app-app
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              app.kubernetes.io/name: traefik
      ports:
        - port: 8081
---
# app → postgres:5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-to-db
  namespace: platform-vibe-my-app
spec:
  podSelector:
    matchLabels:
      app: my-app-postgres
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: my-app-app
      ports:
        - port: 5432
---
# VictoriaMetrics (observability ns) → app:8081 — metrics scrape
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-metrics-scrape-app
  namespace: platform-vibe-my-app
spec:
  podSelector:
    matchLabels:
      app: my-app-app
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: platform-vibe-observability
      ports:
        - port: 8081

If a policy is missing, traffic is dropped silently. Unknown traffic should be denied, not allowed and investigated later. Egress from the cluster to the internet is blocked at cluster level by Platform. No app can exfiltrate data by accident.


The Three-Tier Monitoring Contract

Monitoring is CRITICAL in the contract — not advisory, not recommended. Apps that don’t expose /metrics or don’t log JSON cannot be deployed. The review checks G1–G4 are at the same severity level as image and network violations.

The platform defines a three-tier monitoring model. System metrics are free. Framework and Business metrics are the app’s responsibility.

flowchart LR app-A -->|stdout JSON| Promtail app-B -->|stdout JSON| Promtail Promtail --> Loki Loki --> Grafana app-A -->|"/metrics pull"| VictoriaMetrics app-B -->|"/metrics pull"| VictoriaMetrics VictoriaMetrics --> Grafana app-A -.->|traces| OTEL app-B -.->|traces| OTEL

System Tier — Automatic

cAdvisor runs as a DaemonSet. VictoriaMetrics scrapes it automatically. Every app gets CPU, memory, network I/O, and disk I/O per container — without writing a single line of code.

These metrics answer “is the infrastructure healthy?” They don’t tell you if the app is working.

Framework Tier — App Metrics

The app must expose /metrics on port 8081 in Prometheus format. The pod annotation tells VictoriaMetrics to scrape it:

# Auto-generated by Helm chart — creator does not write this
metadata:
  annotations:
    prometheus.io/scrape: "true"
    prometheus.io/port: "8081"
    prometheus.io/path: "/metrics"

The minimum required metrics — generated by the app-contract skill:

Node.js / TypeScript

import { Registry, Counter, Histogram, Gauge, collectDefaultMetrics } from 'prom-client'

const register = new Registry()
collectDefaultMetrics({ register })

export const httpRequestTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
  registers: [register],
})

export const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request latency',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
  registers: [register],
})

export const activeConnections = new Gauge({
  name: 'http_active_connections',
  help: 'Active HTTP connections',
  registers: [register],
})

// Expose on /metrics
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType)
  res.send(await register.metrics())
})

Python / FastAPI

from prometheus_fastapi_instrumentator import Instrumentator
from prometheus_client import Counter, Histogram

app = FastAPI()

# Default metrics from instrumentator (request count, latency, status codes)
Instrumentator().instrument(app).expose(app)

# Add custom metrics as needed
db_query_total = Counter(
    'db_query_total', 'Total database queries', ['operation', 'table']
)
db_query_duration = Histogram(
    'db_query_duration_seconds', 'Database query duration', ['operation']
)

Go

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequests = prometheus.NewCounterVec(
        prometheus.CounterOpts{Name: "http_requests_total", Help: "Total HTTP requests"},
        []string{"method", "path", "status"},
    )
    httpDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request latency",
            Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 2, 5},
        },
        []string{"method", "path"},
    )
)

func init() {
    prometheus.MustRegister(httpRequests, httpDuration)
}

// Expose on /metrics at port 8081
mux.Handle("/metrics", promhttp.Handler())

These metrics answer “is the application behaving correctly?” — request rate, error rate, latency by endpoint, database query count and duration.

Business Tier — What the App Actually Does

This is the layer that makes monitoring actually useful for the creator. The platform cannot define these metrics — they depend on what the app does. But the contract requires them.

During the skill pipeline, the assistant asks the creator: “What are the 3 most important numbers that tell you if your app is working?” Their answer becomes the metric definitions.

Examples:

  • “How many orders were processed today?” → orders_processed_total Counter with status label
  • “How many items are in low stock?” → inventory_low_stock_items Gauge with category label
  • “How many search queries per minute?” → searches_total Counter
# Python — business metrics for an inventory app
from prometheus_client import Counter, Gauge

orders_processed = Counter(
    'orders_processed_total',
    'Total orders processed',
    ['status']  # fulfilled, cancelled, pending
)

low_stock_items = Gauge(
    'inventory_low_stock_items',
    'Products with stock below threshold',
    ['category']
)

# Call in business logic
orders_processed.labels(status='fulfilled').inc()
low_stock_items.labels(category='electronics').set(current_count)
// Node.js — business metrics for a reporting app
import { Counter, Gauge } from 'prom-client'

const reportsGenerated = new Counter({
  name: 'reports_generated_total',
  help: 'Total reports generated',
  labelNames: ['report_type', 'format'],
})

const activeUsers = new Gauge({
  name: 'active_users_current',
  help: 'Currently active users in the last 15 minutes',
})

These metrics answer “is the app doing what it is supposed to do?” — not just “is it up and fast?” but “is business actually flowing through it?”

The three tiers together give the creator a Grafana dashboard that answers four questions in sequence. Is everything running? Is the infrastructure healthy? Is the app behaving correctly? Is the business flowing? Each question maps to a row, from pod status at the top to business counters at the bottom.

Logging Contract

All logs must go to stdout in JSON format. Required fields: time (ISO8601), level, msg.

Node.js — pino (JSON by default):

import pino from 'pino'
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })

// Every log call produces structured JSON
logger.info({ userId: user.id, action: 'login' }, 'User authenticated')
// → {"time":"2026-01-01T10:00:00Z","level":"info","msg":"User authenticated","userId":"abc123","action":"login"}

Python — python-json-logger:

import logging, sys
from pythonjsonlogger import jsonlogger

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(jsonlogger.JsonFormatter(
    '%(asctime)s %(levelname)s %(message)s',
    rename_fields={"asctime": "time", "levelname": "level", "message": "msg"}
))
logging.root.addHandler(handler)
logging.root.setLevel(logging.INFO)

# Usage
log = logging.getLogger(__name__)
log.info("Order created", extra={"order_id": order.id, "amount": order.total})
# → {"time":"2026-01-01T10:00:00Z","level":"INFO","msg":"Order created","order_id":"ord-123","amount":142.50}

Go — zap (JSON to stdout by default):

import "go.uber.org/zap"
logger, _ := zap.NewProduction()
logger.Info("Order created",
    zap.String("order_id", order.ID),
    zap.Float64("amount", order.Total),
)

File-based logging is explicitly prohibited. No FileHandler, no RotatingFileHandler, no Winston transports.File. File logs are invisible to Loki and lost when the pod restarts.

Loki receives every JSON line from every pod stdout via Promtail DaemonSet. Seven-day retention. The creator queries them in Grafana:

# All logs for this app
{namespace="platform-vibe-my-app"}

# Filter errors
{namespace="platform-vibe-my-app"} | json | level="error"

# Correlate with a trace ID
{namespace="platform-vibe-my-app"} | json | trace_id="abc-123"

The Review Gate — 35+ Checks Before Deploy

The review skill is the deployment gate. CRITICAL findings block deployment — there is no override. WARNING findings are documented and raised to the app owner.

A — Image (all CRITICAL)

CheckWhat it verifies
A1No :latest image tag anywhere
A2Dockerfile declares --platform linux/amd64
A3runAsNonRoot: true in pod securityContext
A4capabilities: { drop: [ALL] } in container securityContext
A5No bare Docker Hub image references in any manifest
A6App image references internal Harbor registry

B — Health & Ports (all CRITICAL)

CheckWhat it verifies
B1containerPort: 8081 declared (application traffic)
B2containerPort: 8082 declared (management)
B3livenessProbe on port 8082 path /liveness
B4readinessProbe on port 8082 path /readiness
B5Probe timeoutSeconds >= 5 (10 if probe checks DB)
B6Port 8082 NOT present in any Ingress rule

C — Networking (all CRITICAL)

CheckWhat it verifies
C1default-deny-ingress NetworkPolicy present
C2Explicit allow: ingress controller → app:8081
C3Explicit allow: observability ns → app:8081 (metrics scrape)
C4Explicit allow: app → DB port (if DB enabled)
C5Explicit allow: app → redis:6379 (if Redis enabled)
C6Explicit allow: app → rabbitmq:5672 (if RabbitMQ enabled)

D — Storage (all CRITICAL)

CheckWhat it verifies
D1PVC exists for every DB before the DB Deployment
D2DB Deployment strategy.type: Recreate (not RollingUpdate — file locks)
D3initContainer waiting for DB before app starts
D4No data in container filesystem (ephemeral — data lost on restart)

E — Secrets (all CRITICAL)

CheckWhat it verifies
E1No kind: Secret in Git — only kind: SealedSecret
E2No passwords or tokens in ConfigMaps
E3No credentials in Dockerfile ENV instructions
E4No CHANGE_ME or REPLACE_ME placeholders remaining
E5Credentials injected via secretKeyRef, never as plain value:
E6SealedSecret metadata.namespace matches deployment namespace
E7Sealed with correct controller name and namespace

G — Metrics & Observability (G1–G4, G6 are CRITICAL)

CheckWhat it verifies
G1Pod has prometheus.io/scrape, prometheus.io/port, prometheus.io/path
G2App actually exposes /metrics in Prometheus format
G3App logs to stdout in JSON format
G4No file-based logging (FileHandler, log file mounts)
G5Log lines contain time, level, msg fields
G6No PII, credentials, or sensitive data patterns in log output

G6 is worth calling out separately. The skill scans recent kubectl logs output for email addresses, credit card patterns, password fields in JSON, and Bearer tokens. A hit triggers a security incident and immediate namespace shutdown — no warnings.

F — Resources and H — Ingress produce WARNINGs (deploy allowed, compliance incomplete).

A typical first review on a vibe-coded app looks like:

⚠️  Found 6 issues in "Inventory Checker". Fix these before deploying:

CRITICAL (must fix):
  [A1] Image tag is ':latest' — use commit SHA (e.g. my-app:a3f91bc)
  [A3] App runs as root — add runAsNonRoot: true, runAsUser: 1000 to pod securityContext
  [C1] default-deny-ingress NetworkPolicy missing from 01-network-policies.yaml
  [E1] Plain Secret found in k8s/02-secrets.yaml — replace with SealedSecret
  [G1] No prometheus.io/scrape annotation on pod — app won't appear in Grafana
  [G3] App logs are plaintext (not JSON) — Loki can't parse them

WARNING (strongly recommended):
  [F2] nginx container has no memory limit — set to 128Mi
  [H1] Ingress URL doesn't match cluster pattern

Run /vibe-review again after fixing.

The creator fixes each finding, re-runs the review, and repeats until it passes. The AI assistant can apply CRITICAL fixes automatically — it modifies the manifest files and the source code without asking.


The Helm Chart — Five Questions, Full Platform

The entire namespace — app container, database, nginx proxy, network policies, RBAC, probes, metrics annotations, HPA — deploys from a single Helm chart. The values.yaml is the only thing the creator touches.

# values.yaml — the five questions
app:
  slug: "inventory-checker"
  name: "Inventory Checker"
  image:
    repository: "inventory-checker-app"
    tag: "a3f91bc"          # commit SHA from CI
  size: md                   # xs / sm / md / lg / xl

cluster:
  prefix: "platform"
  registry: "harbor.internal.example.com"
  mirrorPrefix: "harbor.internal.example.com/dockerhub-proxy"
  observabilityNamespace: "platform-vibe-observability"
  ingressHostPattern: "{prefix}-{slug}.internal.example.com"

postgres:
  enabled: true
  image: "harbor.internal.example.com/dockerhub-proxy/postgres:16"
  storage: 5Gi
  auth:
    existingSecret: "inventory-checker-postgres-secret"

ingress:
  enabled: true
  tls: true

One command:

helm upgrade --install inventory-checker ./helm/vibe-stack -f values.yaml

What Helm renders includes security context baked in — the creator never writes these:

# From the real Helm template (app.yaml)
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
      initContainers:
        - name: wait-for-postgres
          image: mirror/debian:12-slim
          command: ['sh', '-c',
            'until (echo >/dev/tcp/inventory-checker-postgres/5432) 2>/dev/null;
             do echo "waiting for postgres..."; sleep 2; done']
      containers:
        - name: app
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: [ALL]
          livenessProbe:
            httpGet:
              path: /liveness
              port: 8082
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /readiness
              port: 8082
            timeoutSeconds: 5
            failureThreshold: 3

Non-root, no capabilities, management port separated from application port, initContainer waiting for the database. None of this requires the creator to know it exists.


The CI/CD Pipeline — AI App Meets GitOps

The deploy pipeline is fully automatic from the moment it is set up. The creator pushes code; the app is live in approximately two minutes. They never touch the cluster directly.

flowchart LR dev["git push main"] --> secret-scan secret-scan["Trufflehog scan"] --> vuln-scan vuln-scan["Trivy CRITICAL"] --> build build["Build linux/amd64"] --> Harbor Harbor -->|"tag update in values.yaml"| ArgoCD ArgoCD -->|"auto-sync"| Cluster

CI — Build, Scan, Push (generated by the skill pipeline, runs on every push to main):

# .github/workflows/ci.yml — complete real pipeline
name: CI — Build, Scan, Push
on:
  push:
    branches: [main]

jobs:
  build-scan-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set image tag (commit SHA)
        id: meta
        run: echo "tag=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT

      - name: Build image (no push — scan first)
        uses: docker/build-push-action@v5
        with:
          platforms: linux/amd64
          push: false
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}
          outputs: type=docker,dest=/tmp/image.tar

      - name: Check for Docker Hub references
        run: |
          VIOLATIONS=$(grep -rn \
            -e 'image: \+[a-zA-Z0-9._-]*:[a-zA-Z0-9._-]*$' \
            -e 'image: \+docker\.io/' \
            --include="*.yaml" --include="*.yml" --include="Dockerfile*" \
            . | grep -v 'harbor\.' | grep -v 'cluster\.mirrorPrefix' || true)
          if [ -n "$VIOLATIONS" ]; then
            echo "::error::Docker Hub images detected. All images must come from Harbor."
            echo "$VIOLATIONS"
            exit 1
          fi

      - name: Scan secrets with Trufflehog
        uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          severity: CRITICAL      # HIGH advisory in alpha — intentional
          exit-code: 1
          ignore-unfixed: true

      - name: Push image to Harbor
        if: github.ref == 'refs/heads/main'
        uses: docker/build-push-action@v5
        with:
          platforms: linux/amd64
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}

Trivy scans only CRITICAL CVEs, not HIGH. In an alpha environment with vibe-coded apps using whatever base image the AI chose, blocking on HIGH would produce a wall of unfixable findings that discourages deployment entirely. CRITICAL CVEs are genuinely dangerous. HIGH CVEs are visible in the SARIF upload, addressed at upgrade time.

CD — GitOps via ArgoCD (runs after CI succeeds):

# .github/workflows/cd-argocd.yml — does NOT run kubectl or helm
name: CD — Update image tag for ArgoCD sync
on:
  workflow_run:
    workflows: [CI — Build, Scan, Push]
    branches: [main]
    types: [completed]

jobs:
  update-values:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - uses: actions/checkout@v4

      - name: Update image tag in values file
        run: |
          NEW_TAG="${{ github.event.workflow_run.head_sha }}"
          NEW_TAG="${NEW_TAG:0:7}"
          sed -i "s|^  tag:.*|  tag: \"${NEW_TAG}\"|" "helm/vibe-stack/values.yaml"

      - name: Commit and push tag update
        run: |
          git config user.name "vibe-cd-bot"
          git commit -m "chore(deploy): update image to ${NEW_TAG} [skip ci]"
          git push

This workflow does not run kubectl or helm. It commits the new image tag to the repository. ArgoCD watches the repository, detects the change, and syncs the cluster.

# ArgoCD Application — auto-sync with selfHeal
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: inventory-checker
  namespace: argocd
spec:
  source:
    repoURL: https://git.internal.example.com/vibe/inventory-checker
    targetRevision: main
    path: helm/vibe-stack
    helm:
      valueFiles: [values.yaml]
  destination:
    namespace: platform-vibe-inventory-checker
  syncPolicy:
    automated:
      prune: true
      selfHeal: true    # reverts any manual kubectl changes

selfHeal: true means if a creator manually applies something with kubectl apply, ArgoCD reverts it on the next sync cycle. The Git repository is the source of truth. Always.


The Skill Pipeline — AI Onboarding an AI App

Here is where it gets meta. We use an AI coding assistant to transform a vibe-coded app into a contract-compliant deployable workload. The tool onboarding your app is the same kind of tool you used to build it.

The pipeline is seven sequential steps, each a skill file the assistant reads as context.

flowchart TD input(["Vibe App Creator"]) subgraph context["Platform Context"] cv["CLAUDE-vibe-app.md"] pt["PLATFORM-template.md"] end input -->|"name, DB, size, components, runtime"| s1 subgraph skills["Skill Pipeline"] direction TB s0["app-contract"] s1["scaffold"] s2["db-provision"] s3["observability"] s4["gitops"] s_tests["tests"] s5["review — 35+ checks"] s0 --> s1 --> s2 --> s3 --> s4 --> s_tests --> s5 end s5 -->|FAIL| fix["fix and re-review"] fix --> s5 s5 -->|PASS| output["Contract-Compliant App"] context -.->|informs| skills

The K8s manifest structure is deterministic — every app has the same numbered files in the same order:

k8s/
  00-namespace.yaml           ← Namespace + managed-by: vibe-env label
  01-network-policies.yaml    ← default-deny first, then explicit allows
  02-sealed-secrets.yaml      ← SealedSecret stubs with sealing instructions
  03-configmap.yaml           ← Non-sensitive config, LOG_LEVEL, OTEL endpoint
  04-storage.yaml             ← PVC for every stateful component
  05-db.yaml                  ← postgres OR mongo (strategy: Recreate — mandatory)
  06-redis.yaml               ← Redis (if enabled)
  07-rabbitmq.yaml            ← RabbitMQ (if enabled)
  08-app.yaml                 ← Deployment + Service (non-root, resource limits)
  09-nginx.yaml               ← Reverse proxy (Debian-based)
  10-ingress.yaml
  11-metrics-exporters.yaml   ← postgres-exporter, mongodb-exporter, redis-exporter
  12-hpa.yaml

The assistant generates COMPLETE files, never patches. The creator replaces files wholesale. No partial edits, no “add this block at line 47.” Non-technical users cannot apply partial edits reliably.


The RACI Collapse

Traditional platform RACI has three columns: Platform, Product, Security. Each has clearly bounded accountabilities.

In Vibe-Env, a vibe app creator is all three simultaneously. They built the app, they own data governance, they own incident response for their namespace.

What Platform commits to:

  • Cluster maintenance and accepting improvement proposals
  • Onboarding (namespace + kubeconfig + ArgoCD setup)
  • Security violation response (immediate namespace shutdown)
  • Maintaining the observability stack

What Platform does not commit to:

  • Fixing your app when it crashes
  • Any incident response SLA
  • Monitoring your namespace

If your app breaks at 2 AM on a Saturday, nobody fixes it before Monday. This is intentional. This environment is for apps that don’t yet deserve 24/7 attention. If your app needs that, it needs a proper production process.

The path from vibe app to production is not this environment. If an app proves its value:

  1. Architecture reviews the codebase
  2. A professional team takes ownership
  3. Full production process: cloud or on-prem EKS equivalent, SonarQube, OAuth2/SSO, Service Catalog, proper RACI

The vibe prototype may be a complete rewrite informed by what the prototype proved. That is the correct outcome.


Conclusion

The infrastructure problem created by AI agentic development is not going away. More people will build software with these tools. The quality will improve. The velocity will increase.

The organizations that figure out the operational model will capture the value. The ones that block it lose the productivity gain. The ones that let it run unsupervised accumulate technical debt and security risk at a pace that is very hard to recover from.

A purpose-built sandbox with an explicit platform contract is the minimum viable operational model. Not because contracts are bureaucratic overhead, but because they are the thing that makes “yes, build things” a safe answer instead of a deferred problem.

The contract is the boundary between “the AI coded it” and “the platform runs it.” Everything else — the Helm chart, the CI pipeline, the skill pipeline, the review gate — is infrastructure in service of that boundary.

What follows is harder: how does a vibe app become a production service? Who maintains the code when the creator moves on? Those questions don’t have clean answers yet. But they have a prerequisite: a place where the apps can run safely while the answers are worked out.

That prerequisite is solvable. We solved it.