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 aDocumentation 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.
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 amodel-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_eventstable must be running. - Phase 1 (External identity) —
mcp-identity-connectormust be available to resolveactor_idfor attribution in everymodel.callaudit 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 withallow_cloud=false as its immutable default. This means:
- Any request that specifies a provider outside
{vllm_local, mock}is rejected immediately withPOLICY_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=truefor a specific agent or department is a Phase 13 capability and requires an explicit, audited policy configuration change.
Data Classification
Any request carryingdata_classification: restricted or data_classification: confidential must never leave the private network. The gateway enforces this before routing:
| Classification | vllm_local | mock | Cloud provider |
|---|---|---|---|
public | ✅ allowed | ✅ allowed | Requires allow_cloud=true |
internal | ✅ allowed | ✅ allowed | Requires 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 theMCP_RUNTIME_AUDIT_INGEST_URL environment variable to locate the runtime Python audit ingest endpoint:
model.call.failed event instead of model.call. Both event types are forwarded to the same ingest endpoint and stored in audit_events:
| Event type | When emitted |
|---|---|
model.call | Provider responded successfully |
model.call.failed | Provider returned an error or timed out |
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 theprovider field routes to the mock provider. This contract is preserved in Phase 2:
vllm_local provider alongside mock. Existing tests that relied on the PR-1 omission behavior continue to pass unchanged.
Acceptance Criteria
Local model generates a response
Using the Expected — HTTP 200 with a
vllm_local (or mock in CI) provider, confirm the gateway returns a completion:choices array and non-zero token counts.Every model call audited with actor_id, model, and token counts
After the call above, confirm the audit record exists: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.No cloud dependency in code or active environment variables
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.Operational Notes
MCP_RUNTIME_AUDIT_INGEST_URLis 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
mockprovider is strictly for CI and local development without GPU. A misconfigured deployment that runsmockin 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_executionis the only schema change in Phase 2. Apply it before activating the gateway to avoid constraint violations onmodel.callevents that lack an execution context.