Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/joseluis-dev/harness-ai/llms.txt

Use this file to discover all available pages before exploring further.

Phase 2 materializes the Local-first principle that is core to Harness AI: the harness must be able to generate responses without depending on OpenAI, Claude, or any cloud provider. It does this by introducing a model-gateway service that sits between every consumer (Astro, runtime Python, agents, n8n) and the underlying model infrastructure. No consumer ever calls vLLM — or any future provider — directly. The gateway is the single choke point where routing decisions, policy enforcement, and audit records are applied.

Objective

Build a model-gateway service that isolates consumers from the specific model in use, supports the vllm_local provider for local inference and a mock provider for tests, and enforces allow_cloud=false by default so that no model call can reach a cloud provider without an explicit and documented policy exception.

Dependencies

  • Phase 0 (Base executable local) — runtime Python and the audit_events table must be running.
  • Phase 1 (External identity) — mcp-identity-connector must be available to resolve actor_id for attribution in every model.call audit event.

What to Implement

model-gateway service

Standalone service (Python, internal to private-harness-net) that accepts chat and embed requests, selects the appropriate provider, enforces policy, and emits audit events. No external consumer may bypass it.

vllm_local provider

Provider adapter that routes requests to the vLLM instance running on the private network. Used in staging and production where a GPU is available.

mock provider

Deterministic provider that returns fixed responses. Used exclusively in CI and local development without GPU. Never activated in production.

POST /v1/chat endpoint

Chat completion endpoint. Accepts a model identifier, messages array, and optional parameters. Returns the completion and token counts used for audit.

POST /v1/embed endpoint

Embedding generation endpoint. Accepts text and a model identifier; returns a float vector. Used by Phase 5 (RAG) for indexing and retrieval.

model.call audit forwarding

After each model call, the gateway emits a model.call event (or model.call.failed on error) and forwards it to runtime-python via POST /internal/audit/ingest (configured via MCP_RUNTIME_AUDIT_INGEST_URL). The event is stored in audit_events with execution_id=NULL when called outside an execution context.

allow_cloud=false policy

Default policy that blocks any request targeting a cloud provider. Enforced in the gateway; consumers do not need to implement this check.

migration 0004_audit_optional_execution

Makes execution_id nullable in audit_events so model.call events emitted outside an execution context (e.g. direct gateway tests) can be stored without a parent execution.

Policy: allow_cloud=false

The gateway ships with allow_cloud=false as its immutable default. This means:
  • Any request that specifies a provider outside {vllm_local, mock} is rejected immediately with POLICY_DENIED — no model call is made and no tokens are consumed.
  • The policy is enforced in the gateway, not in each consumer. Consumers must not duplicate this check.
  • Activating allow_cloud=true for a specific agent or department is a Phase 13 capability and requires an explicit, audited policy configuration change.
# Attempting to use a cloud provider with allow_cloud=false (default)
curl -fsS -X POST http://model-gateway:8080/v1/chat \
  -H "Content-Type: application/json" \
  -H "X-Harness-Actor-Id: jlopez" \
  -d '{
    "provider": "openai",
    "model": "gpt-4o",
    "messages": [{"role": "user", "content": "Hola"}]
  }'
Expected response — HTTP 403:
{
  "error": "POLICY_DENIED",
  "message": "Cloud provider 'openai' is blocked. allow_cloud=false is the active policy.",
  "provider": "openai"
}

Data Classification

Any request carrying data_classification: restricted or data_classification: confidential must never leave the private network. The gateway enforces this before routing:
Classificationvllm_localmockCloud provider
public✅ allowed✅ allowedRequires allow_cloud=true
internal✅ allowed✅ allowedRequires allow_cloud=true
restricted✅ allowed✅ allowed❌ blocked (POLICY_DENIED)
confidential✅ allowed✅ allowed❌ blocked (POLICY_DENIED)

Audit Flow

Every model invocation produces an audit record regardless of whether it occurs inside an execution context. The gateway uses the MCP_RUNTIME_AUDIT_INGEST_URL environment variable to locate the runtime Python audit ingest endpoint:
Consumer
  → POST /v1/chat (model-gateway)
      → provider call (vllm_local or mock)
      → POST ${MCP_RUNTIME_AUDIT_INGEST_URL}   ← POST /internal/audit/ingest (runtime-python)
          → INSERT INTO audit_events
              (event_type='model.call', actor_id, model,
               tokens_in, tokens_out, execution_id [nullable])
If the provider call fails, the gateway emits a model.call.failed event instead of model.call. Both event types are forwarded to the same ingest endpoint and stored in audit_events:
Event typeWhen emitted
model.callProvider responded successfully
model.call.failedProvider returned an error or timed out
The execution_id=NULL path (migration 0004) allows direct gateway calls — for example, integration tests and admin panel model checks — to be stored in audit_events without a parent execution row.

Provider Compatibility: PR-1 Contract Preserved

PR-1 established that omitting the provider field routes to the mock provider. This contract is preserved in Phase 2:
# PR-1 contract: no provider field → mock
curl -fsS -X POST http://model-gateway:8080/v1/chat \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mock-llm-v1",
    "messages": [{"role": "user", "content": "ping"}]
  }'
PR-2 adds the vllm_local provider alongside mock. Existing tests that relied on the PR-1 omission behavior continue to pass unchanged.

Acceptance Criteria

1

Local model generates a response

Using the vllm_local (or mock in CI) provider, confirm the gateway returns a completion:
curl -fsS -X POST http://model-gateway:8080/v1/chat \
  -H "Content-Type: application/json" \
  -H "X-Harness-Actor-Id: jlopez" \
  -d '{
    "provider": "vllm_local",
    "model": "mistral-7b-instruct",
    "messages": [{"role": "user", "content": "¿Qué es Harness AI?"}]
  }'
Expected — HTTP 200 with a choices array and non-zero token counts.
2

Every model call audited with actor_id, model, and token counts

After the call above, confirm the audit record exists:
curl -fsS \
  "http://localhost:4321/api/audit/events?event_type=model.call&limit=1"
Expected — the latest event includes actor_id, model, tokens_in, and tokens_out. execution_id may be null for direct gateway calls. A failed call produces event_type=model.call.failed instead.
3

No cloud dependency in code or active environment variables

# Confirm no direct cloud SDK imports in the codebase
grep -rn "import openai\|from anthropic\|import google.generativeai" \
  apps/ packages/ --include="*.py" --include="*.ts"
Expected — zero matches. Cloud SDK imports are not present in active code paths.
# Confirm OPENAI_API_KEY and ANTHROPIC_API_KEY are not set
printenv | grep -E "OPENAI_API_KEY|ANTHROPIC_API_KEY"
Expected — no output (variables not set in the default environment).
4

Cloud provider request blocked with POLICY_DENIED

Issue the cloud provider request shown in the policy section above. Confirm HTTP 403 with error: POLICY_DENIED and verify the blocked attempt is recorded in audit_events.
5

POST /v1/chat and POST /v1/embed documented and consumable

Confirm both endpoints respond from runtime Python:
# Embed endpoint
curl -fsS -X POST http://model-gateway:8080/v1/embed \
  -H "Content-Type: application/json" \
  -H "X-Harness-Actor-Id: system" \
  -d '{
    "provider": "vllm_local",
    "model": "bge-m3",
    "input": "Texto de prueba para embedding"
  }'
Expected — HTTP 200 with an embedding float array.

Operational Notes

The rule is absolute: every call to a language model or embedding model passes through the Model Gateway. No service — not Astro, not n8n, not any MCP, not the runtime Python worker — may call vLLM or any provider SDK directly. This invariant is verified by a grep check and is a blocking condition for architectural maturity (condition #7 in references/04-architectural-maturity.md).
  • MCP_RUNTIME_AUDIT_INGEST_URL is the environment variable the model-gateway reads to locate the runtime Python audit ingest endpoint (e.g. http://runtime-python:8000/internal/audit/ingest). It must be set in the gateway’s environment; the endpoint must be internal-only and never exposed on the public network.
  • The mock provider is strictly for CI and local development without GPU. A misconfigured deployment that runs mock in production produces no error — it silently returns fake responses. Guard against this with an environment variable check at startup.
  • The gateway’s provider catalog grows in Phase 13 (external providers). The routing and policy architecture established here must not be refactored when that happens — Phase 13 adds providers to the existing model, it does not replace it.
  • Migration 0004_audit_optional_execution is the only schema change in Phase 2. Apply it before activating the gateway to avoid constraint violations on model.call events that lack an execution context.

Build docs developers (and LLMs) love