Skip to content
browse the docs

Vanzik Wire Protocol — v1

vanzik v2.0updated view on github

This document is the operator-facing description of the JSON wire format for the Vanzik /v1/check endpoint. It is the public contract between the proxy (Go) and any client SDK (Python, TypeScript, or third-party).

The machine-readable spec lives at pkg/proxy/schema/v1/schema.json (JSON Schema draft-07). The canonical Go types live in pkg/policy (ActionRequest, CheckResult) and are re-exported as type aliases from pkg/proxy/schema/v1.

Schema versioning#

Every response carries schema_version: "v1". Requests may omit it (the first-party SDKs do; see below). The server accepts any of:

  • schema_version omitted → defaulted to "v1"
  • schema_version: "v1" → accepted
  • any other value → rejected with HTTP 400 and a structured error body
    {"error":"unsupported schema_version; expected v1","received":"v2"}
    

The constant lives at pkg/proxy.SchemaVersionV1 (Go) and pkg/proxy/schema/v1.Version.

Evolution policy#

v1 is frozen:

  1. No field is ever removed.
  2. No field's JSON name or type ever changes.
  3. New optional fields may be added under v1. SDKs MUST tolerate unknown response fields (ignore-and-pass-through). Servers MUST tolerate unknown request fields (the Go decoder does this by default).
  4. Breaking changes ship as a separate pkg/proxy/schema/v2 package with its own Version = "v2" constant. The proxy will negotiate between v1 and v2 by inspecting schema_version on the request.

Strict clients that reject unknown response fields (a security-hardened mode you opt in to) are responsible for coordinating their upgrade window with their server.

Cross-language contract test#

A single set of fixtures under pkg/proxy/schema/v1/testdata/ is loaded by tests in all three languages. Drift between any pair (Go ↔ Python ↔ TypeScript) fails CI:

Language Test file
Go pkg/proxy/schema/v1/types_test.go
Python plugins/python/tests/test_wire_format.py
TypeScript plugins/typescript/src/__tests__/wire_format.test.ts

POST /v1/check request — ActionRequest#

Fields are JSON-encoded; only scope is required.

Field Type Required Description
schema_version string ("v1") optional Wire-format version. Empty → defaulted to "v1"; any other value → 400.
scope string required Policy rule scope. Built-ins: filesystem, shell, network, browser, cost, data, mcp_tool (used by the MCP gateway). Custom scopes are valid if defined in policy.
action string optional Action verb for action-style scopes (e.g. read, write, delete on filesystem).
command string optional Shell command string for the shell scope. Matched against rule pattern.
path string optional Filesystem path. The server URL-decodes %HH once, then rejects any .. segment after filepath.Clean.
domain string optional Hostname for the network / browser scope.
url string optional Full URL for the network scope.
agent_id string optional Stable agent identifier. Used as a per-agent override key and as a rate-limit key.
session_id string optional Required to use cost.max_per_session accumulation; also keyed by audit Query.
parent_session_id string optional The session that started this one (an orchestrator's session for a sub-agent). Not used by policy; recorded in the audit entry's request so GET /v1/sessions can link parent and child sessions. Omitted when empty, so requests without it are unchanged.
est_cost number ≥ 0 optional Estimated cost of this action in USD. Required for the cost scope. SDKs drop this field when its value is 0 to avoid spurious cost-scope evaluations.
meta object<string,string> optional Free-form string-keyed metadata. Forwarded to notifiers and the audit log; may be redacted by notify.DefaultRedactor.

Example#

{
  "schema_version": "v1",
  "agent_id": "test-agent-001",
  "session_id": "sess-abc",
  "scope": "shell",
  "command": "ls -la",
  "meta": {"source": "ci-fixture"}
}

This is the canonical fixture at pkg/proxy/schema/v1/testdata/sample_request.json.

POST /v1/check response — CheckResult#

Field Type Always set Description
schema_version string ("v1") yes Wire-format version emitted by the server.
decision enum yes One of "ALLOW", "DENY", "REQUIRE_APPROVAL".
reason string yes Human-readable explanation. Stable enough for log aggregation but not part of the policy contract — wording may evolve.
matched_rule string usually Identifier of the rule that produced the decision (e.g. "allow:shell:ls", "deny:filesystem:path_traversal", "deny:ratelimit:shell"). Empty only on synthetic results that do not correspond to a single rule.
approval_id string on REQUIRE_APPROVAL, and on the ALLOW / DENY a replayed approval returns (matched_rule allow:approved / deny:approved); omitted otherwise Format: ap_ followed by 32 hex chars. Pass to /v1/approve/{id}, /v1/deny/{id}, /v1/status/{id}.
approval_url string (URL) only when decision == REQUIRE_APPROVAL Absolute URL clients can present to a human reviewer.

Example#

{
  "schema_version": "v1",
  "decision": "ALLOW",
  "reason": "matched allow rule",
  "matched_rule": "allow:shell:ls"
}

This is the canonical fixture at pkg/proxy/schema/v1/testdata/sample_result.json.

Backward-compatibility rules#

These rules apply to every change to v1, regardless of who proposes it:

  • Never remove a field. Even unused fields stay on the wire — a client that reads them today must keep reading them after upgrade.
  • Never change a field's type. A string field stays string; a number stays number. Nullability does not change.
  • Never change a field's JSON name. Renames ship as v2.
  • New optional fields are allowed. They MUST default to absent (omitempty on the Go side; the equivalent on Python and TS). Existing SDK versions MUST continue to function with the new field absent or unknown.
  • Required fields stay required. Adding a new required field is a breaking change and must wait for v2.
  • Decision enum stays closed. Adding REVIEW or RATE_LIMITED to the response decision is a breaking change for every client that switches on the value. Use existing values plus matched_rule granularity (e.g. deny:ratelimit:shell) instead.

SDK behavior#

Both first-party SDKs (Python and TypeScript) emit a body whose key set is a subset of the v1 schema. Specifically:

  • They emit snake_case JSON keys (the Go server is strict about names).
  • They drop est_cost when its value is 0.
  • They do not currently emit schema_version; the server defaults it. This is a documented optimization — adding schema_version: "v1" to every request is a no-op and forward-compatible.
  • Both fail closed by default when the proxy is unreachable (DENY decision with reason: "Vanzik unreachable (deny): …" from Python, "Vanzik unreachable: …" from TypeScript). Both accept fail-open for callers whose threat model treats Vanzik as best-effort: Guard(fail_mode="allow") in Python, failMode: 'allow' in TypeScript. The decision is then ALLOW, still with a Vanzik unreachable reason ((allow) in Python's).