From e2827ff3ee688097be698b173e269dbf76a8ded5 Mon Sep 17 00:00:00 2001 From: Michael Heller Date: Mon, 3 Aug 2026 05:52:41 -0400 Subject: [PATCH] =?UTF-8?q?feat(inference):=20local=20InferenceGateway=20a?= =?UTF-8?q?dapter=20=E2=80=94=20fail-closed,=20audits=20every=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local (agent-machine/Noetica) adapter of the InferenceGateway intersection seam (sourceos-spec inference-gateway-intersection.md). Serves only through a registered active InferenceProvider + an admitting consent decision, and emits a GatewayCallAudit (memory-mesh v0.1) on EVERY call including refusals — nothing served un-consented or un-audited. scripts/validate-inference-gateway.py (17 negative+positive controls) wired into make validate. Output returned only on outcome=ok. --- Makefile | 3 + scripts/validate-inference-gateway.py | 63 +++++++++++++++ src/agent_machine/inference_gateway.py | 101 +++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 scripts/validate-inference-gateway.py create mode 100644 src/agent_machine/inference_gateway.py diff --git a/Makefile b/Makefile index 1e9ecbf..10e80bd 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,9 @@ validate-artifact-digest-honesty: validate-consent-before-staging: $(PYTHON) scripts/validate-consent-before-staging.py +validate-inference-gateway: + $(PYTHON) scripts/validate-inference-gateway.py + validate-inference-receipt: $(PYTHON) scripts/validate-inference-receipt.py diff --git a/scripts/validate-inference-gateway.py b/scripts/validate-inference-gateway.py new file mode 100644 index 0000000..ba1228f --- /dev/null +++ b/scripts/validate-inference-gateway.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Validate the local InferenceGateway adapter refuses and audits, fail-closed. + +The property asserted is a refusal: a call served without a registered active +provider, or without an admitting consent decision, or without a backend, must not +return output — and must still emit a GatewayCallAudit (outcome="denied"). A gateway +never observed refusing is indistinguishable from no gateway. One positive case +confirms an admitted call returns output plus an outcome="ok" audit. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SRC_ROOT = REPO_ROOT / "src" +if str(SRC_ROOT) not in sys.path: + sys.path.insert(0, str(SRC_ROOT)) + +from agent_machine.inference_gateway import serve, GATEWAY_AUDIT_REQUIRED # noqa: E402 + +ACTIVE = {"id": "local-ollama", "kind": "InferenceProvider", "status": "active"} +OK_REQ = { + "model": "llama-3.3-70b", "caller": "human:michael", "purpose": "discover", + "space": "user-space", "input": "hi", + "consent": {"policyDecisionRef": "urn:srcos:policy-decision:abc"}, +} +def _backend(_req): + return {"output": "hello", "usage": {"tokens": 3}} + +failures = [] +def check(name, cond): + print((" ok " if cond else " FAIL ") + name) + if not cond: + failures.append(name) + +def audit_ok(a): + return isinstance(a, dict) and all(k in a for k in GATEWAY_AUDIT_REQUIRED) + +# ── negative controls: every refusal returns no output + a denied audit ── +for name, req, prov, backend in [ + ("no consent → refused", {**OK_REQ, "consent": {}}, ACTIVE, _backend), + ("no provider → refused", OK_REQ, None, _backend), + ("inactive provider → refused", OK_REQ, {**ACTIVE, "status": "draft"}, _backend), + ("no backend → refused", OK_REQ, ACTIVE, None), + ("missing field (model) → refused", {k: v for k, v in OK_REQ.items() if k != "model"}, ACTIVE, _backend), + ("non-dict request → refused", "nope", ACTIVE, _backend), + ("backend raises → audited denial", OK_REQ, ACTIVE, (lambda _r: (_ for _ in ()).throw(RuntimeError()))), +]: + resp, audit = serve(req, provider=prov, backend=backend) + check(name + " [no output]", resp is None) + check(name + " [denied audit]", audit_ok(audit) and audit["outcome"] == "denied") + +# ── positive control: an admitted call returns output + an ok audit ── +resp, audit = serve(OK_REQ, provider=ACTIVE, backend=_backend) +check("admitted call returns output", resp is not None and resp.get("output") == "hello") +check("admitted call ok audit", audit_ok(audit) and audit["outcome"] == "ok") +check("response carries receipt_hash == audit", resp and resp.get("receipt_hash") == audit["receipt_hash"]) + +if failures: + print(f"\nFAILED: {len(failures)} check(s)") + sys.exit(1) +print("\nOK: InferenceGateway local adapter fails closed and audits every call") diff --git a/src/agent_machine/inference_gateway.py b/src/agent_machine/inference_gateway.py new file mode 100644 index 0000000..fa2ad32 --- /dev/null +++ b/src/agent_machine/inference_gateway.py @@ -0,0 +1,101 @@ +"""Local adapter for the InferenceGateway intersection seam. + +Both planes serve inference through one contract (sourceos-spec +`inference-gateway-intersection.md`); this is the **local** adapter (agent-machine / +Noetica). It serves a call only through a registered, active `InferenceProvider` +(`contracts/inference-provider.schema.json`) and only when an admitting consent +decision is present — and it emits a `GatewayCallAudit` (memory-mesh +`gateway-call-audit.schema.json` v0.1) on **every** call, including refusals, so that +nothing is ever served un-consented or un-audited. + +The controls here are refusals. A gateway never observed denying a call is +indistinguishable from no gateway — so a *denied* call still returns a full audit +(outcome="denied") and no output. Output is returned only on outcome="ok". +""" +from __future__ import annotations + +import datetime +from typing import Any, Callable, Dict, Optional, Tuple + +from agent_machine.digest import stable_digest + +# GatewayCallAudit v0.1 required fields (memory-mesh schemas/gateway-call-audit.schema.json). +GATEWAY_AUDIT_REQUIRED = [ + "schemaVersion", "recordType", "callId", "call_type", "outcome", "caller", + "model", "epistemicLevel", "recalled_count", "written", "occurred_at", "receipt_hash", +] +_ACTIVE_PROVIDER_STATUS = {"active", "ready", "serving", "enabled"} + + +def _now() -> str: + return datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def _audit(request: Dict[str, Any], outcome: str, error: Optional[str] = None) -> Dict[str, Any]: + """Build a GatewayCallAudit record. Always fully populated — even for refusals.""" + core = { + "schemaVersion": "0.1", + "recordType": "GatewayCallAudit", + "call_type": "inference", + "outcome": outcome, + "caller": (request or {}).get("caller", "unknown"), + "model": (request or {}).get("model", "unknown"), + "epistemicLevel": (request or {}).get("epistemicLevel", "operational"), + "recalled_count": 0, + "written": False, + "occurred_at": _now(), + } + if error: + core["error"] = error + core["callId"] = stable_digest({"c": core["caller"], "m": core["model"], "t": core["occurred_at"], "o": outcome})[7:23] + core["receipt_hash"] = stable_digest(core) + return core + + +def _refuse(request: Dict[str, Any], reason: str) -> Tuple[None, Dict[str, Any]]: + return None, _audit(request, "denied", error=reason) + + +def serve( + request: Dict[str, Any], + *, + provider: Optional[Dict[str, Any]] = None, + backend: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None, +) -> Tuple[Optional[Dict[str, Any]], Dict[str, Any]]: + """Serve one inference call, fail-closed. Returns (response|None, audit). + + Output is returned only when the call is admitted (outcome="ok"); every other + path returns (None, denied-audit). Never raises for a bad request — it refuses. + """ + if not isinstance(request, dict): + return _refuse({}, "request is not an object") + for field in ("model", "caller", "purpose", "space"): + if not request.get(field): + return _refuse(request, f"request missing required field: {field}") + + consent = request.get("consent") + if not isinstance(consent, dict) or not consent.get("policyDecisionRef"): + return _refuse(request, "no admitting consent decision — refused fail-closed") + + if not isinstance(provider, dict): + return _refuse(request, "no registered InferenceProvider — refused fail-closed") + if provider.get("status") not in _ACTIVE_PROVIDER_STATUS: + return _refuse(request, f"provider status {provider.get('status')!r} is not active") + + if not callable(backend): + return _refuse(request, "no serving backend available") + + try: + result = backend(request) + except Exception as exc: # a backend failure is an audited denial, not a crash + return _refuse(request, f"backend error: {type(exc).__name__}") + if not isinstance(result, dict) or "output" not in result: + return _refuse(request, "backend returned no output") + + audit = _audit(request, "ok") + response = { + "output": result["output"], + "usage": result.get("usage", {}), + "receipt_hash": audit["receipt_hash"], + } + return response, audit