SDKs

Official client libraries for Python, TypeScript, LangChain/LangGraph, the OpenAI Agents SDK, CrewAI, Pydantic AI, Microsoft Agent Framework, and the Vercel AI SDK — plus an MCP server for Claude Code and Claude Desktop. A Java SDK is in development. Submit intents, retrieve decisions, handle errors — with full type safety.

At a Glance

Language Package Install Requirements
Python gaas-sdk pip install gaas-sdk Python 3.11+
TypeScript @governancehq/sdk npm install @governancehq/sdk Node.js 18+
Java is.gaas:gaas-sdk is.gaas:gaas-sdk from https://maven.gaas.is Java 17+
LangChain / LangGraph gaas-langchain pip install gaas-langchain Python 3.11+
OpenAI Agents SDK gaas-openai-agents pip install gaas-openai-agents Python 3.11+
MCP (Claude Code / Desktop) @governancehq/mcp npx -y @governancehq/mcp Node.js 18+
CrewAI gaas-crewai pip install gaas-crewai Python 3.11+
Vercel AI SDK @governancehq/vercel-ai npm install @governancehq/vercel-ai Node.js 18+, ai v7
Pydantic AI gaas-pydantic-ai pip install gaas-pydantic-ai Python 3.11+
Microsoft Agent Framework gaas-agent-framework pip install "gaas-agent-framework[agent-framework]" Python 3.11+, agent-framework-core 1.15+
All SDKs are Apache-2.0 licensed. Source and full documentation at github.com/H2OmAI/gaas under sdks/.
Framework plugins fail closed. If GaaS cannot give a decision — it is unreachable, it times out, or it answers with any HTTP error, including a wrong API key (401) — the governed tool does not run. The plugin reports it exactly as a BLOCK, with GovernanceBlockedError and verdict UNEVALUATED (plus a short reason such as HTTP 401 or timeout). To run tools anyway when GaaS cannot answer, set fail_open=True (Python) or failOpen: true (Vercel AI); the action then runs ungoverned and a warning is logged. raise_on_governance_error / raiseOnGovernanceError still re-raises the underlying error and takes precedence. This is the default from gaas-langchain 0.3.0, gaas-crewai 0.2.0, gaas-openai-agents 0.2.0, gaas-pydantic-ai 0.2.0 and @governancehq/vercel-ai 0.2.0; earlier versions ran the tool ungoverned on any error. gaas-agent-framework has failed closed from its first release; its raise_on_governance_error raises GaaSGovernanceError with the underlying error as the cause, because Agent Framework would otherwise turn the error into a tool result and keep the run going.

Python

Install

pip install gaas-sdk

Submit an Intent

from gaas_sdk import GaaSClient, build_intent, ActionType, TargetType

async with GaaSClient(
    "https://api.gaas.is",
    headers={"X-API-Key": "your_key"},
) as client:
    intent = build_intent(
        agent_id="billing_bot",
        action_type=ActionType.COMMUNICATE,
        verb="send_email",
        target_type=TargetType.PERSON,
        target_identifier="patient@example.com",
        summary="Send billing statement to patient",
        content={"recipient": "patient@example.com", "channel": "email"},
    )
    response = await client.submit_intent(intent)

    if response.data.verdict == "approve":
        send_email(response.data)
    elif response.data.verdict == "block":
        log(response.data.reasoning)

Sync Client

A synchronous client is available for non-async codebases:

from gaas_sdk import GaaSClientSync

with GaaSClientSync(
    "https://api.gaas.is",
    headers={"X-API-Key": "your_key"},
) as client:
    response = client.submit_intent(intent)

Bulk Submission

Submit up to 50 intents concurrently with partial failure support:

intents = [build_intent(...) for _ in range(10)]
response = await client.submit_intents_bulk(intents)

for result in response.data.results:
    if result.success:
        print(f"Decision: {result.decision.verdict}")
    else:
        print(f"Error: {result.error}")

Field Filtering

Request only specific fields to reduce response size:

response = await client.submit_intent(
    intent,
    fields="verdict,reasoning.summary,risk_assessment.overall_score"
)

Idempotency

Prevent duplicate submissions with idempotency keys (header-based recommended):

# Header-based (recommended)
response = await client.submit_intent(
    intent,
    headers={"Idempotency-Key": "unique-key-123"}
)

# Body-based (legacy)
intent = build_intent(
    idempotency_key="unique-key-123",
    agent_id="my-agent",
    # ...
)

Error Handling

from gaas_sdk import (
    GaaSError,
    GaaSValidationError,
    GaaSSemanticError,
    GaaSNotFoundError,
    GaaSConflictError,
    GaaSServerError,
    GaaSConnectionError,
)

try:
    response = await client.submit_intent(intent)
except GaaSValidationError as e:
    print(f"Validation failed (400): {e.message}")
except GaaSSemanticError as e:
    print(f"Semantic error (422): {e.message}")
except GaaSConflictError as e:
    print(f"Conflict (409): {e.message}")
except GaaSConnectionError:
    print("Could not reach GaaS server")
except GaaSError as e:
    # Catches all other GaaS errors (401, 402, 429, 500, etc.)
    print(f"GaaS error {e.code}: {e.message}")

Best Practices


TypeScript

Install

npm install @governancehq/sdk

Submit an Intent

import { GaaSClient, buildIntent, ActionType, TargetType } from '@governancehq/sdk';

const client = new GaaSClient({
  baseUrl: 'https://api.gaas.is',
  headers: { 'X-API-Key': 'your_key' },
});

const intent = buildIntent({
  agentId: 'billing_bot',
  actionType: ActionType.Communicate,
  verb: 'send_email',
  targetType: TargetType.Person,
  targetIdentifier: 'patient@example.com',
  summary: 'Send billing statement to patient',
  content: { recipient: 'patient@example.com', channel: 'email' },
});

const response = await client.submitIntent(intent);

if (response.data.verdict === 'approve') {
  sendEmail(response.data);
} else if (response.data.verdict === 'block') {
  console.log(response.data.verdictReason);
}
Automatic case conversion. The TypeScript SDK converts between camelCase (JavaScript) and snake_case (API) automatically. buildIntent({ agentId }) sends agent_id over the wire; response.data.riskAssessment comes from risk_assessment.

Error Handling

import { GaaSValidationError, GaaSConnectionError } from '@governancehq/sdk';

try {
  const response = await client.submitIntent(intent);
} catch (error) {
  if (error instanceof GaaSValidationError) {
    console.error(`Validation failed: ${error.message}`);
  } else if (error instanceof GaaSConnectionError) {
    console.error('Could not reach GaaS server');
  }
}

Java

The Java SDK (version 0.3.1, Java 17+) installs from the GaaS Maven repository, https://maven.gaas.is, alongside Maven Central. Every published file has .sha256 and .sha512 checksums, and a published version never changes.

Install

Maven (pom.xml):

<repositories>
    <repository>
        <id>gaas</id>
        <url>https://maven.gaas.is</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>is.gaas</groupId>
        <artifactId>gaas-sdk</artifactId>
        <version>0.3.1</version>
    </dependency>
</dependencies>

Gradle (build.gradle):

repositories {
    mavenCentral()
    maven {
        url = 'https://maven.gaas.is'
        content { includeGroup 'is.gaas' }
    }
}

dependencies {
    implementation 'is.gaas:gaas-sdk:0.3.1'
}

Gradle Kotlin DSL (build.gradle.kts):

repositories {
    mavenCentral()
    maven("https://maven.gaas.is") {
        content { includeGroup("is.gaas") }
    }
}

dependencies {
    implementation("is.gaas:gaas-sdk:0.3.1")
}

includeGroup makes Gradle ask the GaaS repository for is.gaas artifacts only, and Maven Central for everything else.

Submit an Intent

With is.gaas.sdk.*, is.gaas.sdk.model.*, is.gaas.sdk.model.enums.* and java.util.Map imported, and your API key in GAAS_API_KEY:

try (GaaSClient client = new GaaSClient("https://api.gaas.is", System.getenv("GAAS_API_KEY"))) {
    IntentDeclaration intent = IntentBuilder.create()
        .agentId("onboarding-agent")
        .actionType(ActionType.COMMUNICATE)
        .verb("send")
        .targetType(TargetType.ENDPOINT)
        .targetIdentifier("email-service")
        .summary("Send the welcome email to a new customer")
        .content(Map.of("to", "user@example.com", "template", "welcome"))
        .build();

    GovernanceDecision decision = client.submitIntent(intent).getData();
    System.out.println(decision.getVerdict()); // APPROVE, APPROVE_MODIFIED, ESCALATE or BLOCK
}

Act on the Verdict

GaaS decides; your code enforces. Act only on APPROVE, or on APPROVE_MODIFIED with the changes applied. A deliberated decision takes about 40–60 s, so the client waits up to 240 s by default (timeout on the builder; see Authentication).

switch (decision.getVerdict()) {
    case APPROVE -> act(intent);
    case APPROVE_MODIFIED -> act(intent, decision.getModifications()); // apply the changes first
    case ESCALATE -> holdForReview(decision.getEscalation().getEscalationId()); // a person decides
    case BLOCK -> refuse(decision.getBlock().getReasons()); // do not act
}

Async API

Every method has an …Async twin that returns a CompletableFuture:

client.submitIntentAsync(intent)
    .thenAccept(response -> handle(response.getData(), intent))
    .exceptionally(error -> {
        // A failure arrives wrapped in a CompletionException; the GaaSException is its cause.
        Throwable cause = error instanceof CompletionException ? error.getCause() : error;
        noDecision(cause); // do not act
        return null;
    })
    .join();

Error Handling

Every failure is an unchecked GaaSException. If GaaS gives no decision, do not act.

ExceptionHTTPCode
GaaSValidationException400validation_error
GaaSAuthenticationException401unauthorized
GaaSQuotaExceededException402quota_exceeded (getUsed(), getHardLimit())
GaaSForbiddenException403forbidden
GaaSNotFoundException404not_found
GaaSConflictException409conflict
GaaSSemanticException422semantic_validation_error
GaaSRateLimitException429rate_limit_exceeded (getRetryAfterSeconds())
GaaSServerException500internal_error
GaaSConnectionException—connection_error: could not connect, or no answer in time
try {
    GovernanceDecision decision = client.submitIntent(intent).getData();
    handle(decision, intent);
} catch (GaaSRateLimitException e) {
    retryAfter(e.getRetryAfterSeconds()); // 429; null if the API gave no wait
} catch (GaaSQuotaExceededException e) {
    report("quota used " + e.getUsed() + " of " + e.getHardLimit()); // 402
} catch (GaaSAuthenticationException | GaaSForbiddenException e) {
    report("check the API key and its role: " + e.getMessage()); // 401, 403
} catch (GaaSException e) {
    report(e.getCode() + ": " + e.getMessage()); // anything else, including no connection
}
// Whatever was caught, the action did not run: no decision means no permission.

LangChain / LangGraph

Install

pip install gaas-langchain

Optional extras: pip install gaas-langchain[langchain], gaas-langchain[langgraph], or gaas-langchain[all].

Configuration

from gaas_langchain import GaaSGovernanceConfig

config = GaaSGovernanceConfig(
    api_url="https://api.gaas.is",
    api_key="gsk_your_key",
    agent_id="my-langchain-agent",
    block_on_escalate=True,     # raise on ESCALATE verdicts too
    timeout_seconds=240.0,  # enough for a deliberated decision (30-60 s)
    sensitivity="INTERNAL",
    fail_open=False,        # no decision from GaaS: block the tool (default)
)

If GaaS cannot give a decision, the tool, node or (with enforce=True) callback raises GovernanceBlockedError with verdict UNEVALUATED and the tool does not run. Set fail_open=True to run it anyway, ungoverned (see Framework plugins fail closed).

Govern a Single Tool

from gaas_langchain import govern_tool

safe_tool = govern_tool(my_tool, config=config)
# safe_tool.run() and safe_tool.arun() now submit governance
# intents before execution. Blocked actions raise GovernanceBlockedError.

Govern Multiple Tools

from gaas_langchain import govern_tools

safe_tools = govern_tools([tool_a, tool_b, tool_c], config=config)

LangGraph Node Decorator

from gaas_langchain import govern_node

@govern_node(config=config, node_name="send_email_node", financial_exposure_usd=0.0)
async def send_email(state):
    # Only executes if governance approves
    return {"status": "sent"}

Supports both sync and async node functions. Uses functools.wraps to preserve function metadata. Additional parameters: sensitivity, regulatory_domains.

Callback Handler (Observability)

from gaas_langchain import GaaSCallbackHandler

handler = GaaSCallbackHandler(config, enforce=False)
# Pass as a LangChain callback — logs all governance decisions
# Set enforce=True to raise GovernanceBlockedError on BLOCK/ESCALATE

# After agent run:
print(handler.summary())
# {"total_tool_calls": 5, "approved": 4, "blocked": 1, "block_rate": 0.2, ...}
handler.reset()  # clear log between runs

Error Handling

from gaas_langchain import GovernanceBlockedError

try:
    result = safe_tool.run("send payment")
except GovernanceBlockedError as e:
    print(e.verdict)              # "BLOCK", "ESCALATE", ... or "UNEVALUATED"
    print(e.reason)               # UNEVALUATED only: "HTTP 401", "timeout", ...
    print(e.decision_id)           # GaaS decision ID
    print(e.risk_score)             # 0.0–1.0
    print(e.blocking_policies)      # ["pol_t1_002", ...]
    print(e.governance_proof_token)  # ECDSA-signed proof token ID

OpenAI Agents SDK

Install

pip install gaas-openai-agents[openai-agents]

Govern Function Tools

from agents import Agent, Runner, function_tool
from gaas_openai_agents import govern_tools, GaaSGovernanceConfig

config = GaaSGovernanceConfig(
    api_url="https://api.gaas.is",
    api_key="gsk_your_key",
    agent_id="my-agent",
)

agent = Agent(
    name="assistant",
    tools=govern_tools([search_tool, email_tool], config=config),
)
result = await Runner.run(agent, "...")

Every tool call submits a governance intent before executing. Tool metadata (name, description, schema, guardrails) is preserved. Only FunctionTool instances can be governed — hosted tools (web search, code interpreter) execute on OpenAI's side.

BLOCK Halts the Run

from gaas_openai_agents import GovernanceBlockedError

try:
    result = await Runner.run(agent, "wire $250k to the new vendor")
except GovernanceBlockedError as e:
    print(e.verdict)               # "BLOCK", "ESCALATE", "ESCALATE_DENY", ...
    print(e.decision_id)            # GaaS decision ID
    print(e.blocking_policies)      # ["pol_t1_002", ...]
    print(e.governance_proof_token)  # ECDSA-signed proof token ID

GovernanceBlockedError subclasses the framework's AgentsException, so it propagates out of Runner.run() unchanged — a blocked action halts the run instead of becoming error text the model can route around. Errors inside your own tool functions keep the framework's default behaviour (formatted for the model, run continues). When GaaS cannot give a decision, the run halts the same way, with verdict UNEVALUATED, unless fail_open=True (see Framework plugins fail closed).

Hold-and-Poll on ESCALATE

config = GaaSGovernanceConfig(
    api_key="gsk_your_key",
    agent_id="my-agent",
    hold_on_escalate=True,          # wait for the human decision
    escalation_poll_seconds=5.0,
    escalation_max_wait_seconds=600.0,
)

On ESCALATE the tool call holds while GaaS routes the escalation to a human reviewer. Approve/modify lets the tool execute; deny or timeout raises GovernanceBlockedError with verdict ESCALATE_DENY or ESCALATE_TIMEOUT.


CrewAI

Install

pip install gaas-crewai[crewai]

Govern Crew Tools

from crewai import Agent
from gaas_crewai import govern_tools, GaaSGovernanceConfig

config = GaaSGovernanceConfig(
    api_url="https://api.gaas.is",
    api_key="gsk_your_key",
    agent_id="my-crew-agent",
)

researcher = Agent(
    role="researcher",
    goal="...",
    backstory="...",
    tools=govern_tools([search_tool, email_tool], config=config),
)

Works with hand-written BaseTool subclasses and @tool-decorated functions alike. Name, description, and args_schema are preserved — the agent sees an identical tool.

Verdict Semantics

On BLOCK the wrapped tool never executes and GovernanceBlockedError is raised from the tool. Inside a running Crew, the framework's tool-usage loop surfaces the block to the agent as an error observation and may retry — each retry submits a fresh intent that is re-blocked, and every attempt lands on the signed audit chain. Hold-and-poll on ESCALATE is supported via hold_on_escalate=True (approve/modify proceeds; deny/timeout raises ESCALATE_DENY / ESCALATE_TIMEOUT). When GaaS cannot give a decision, the tool does not execute either: GovernanceBlockedError with verdict UNEVALUATED, unless fail_open=True (see Framework plugins fail closed).

Error Handling

from gaas_crewai import GovernanceBlockedError

try:
    result = governed_tool.run(to="all-customers@list", subject="...")
except GovernanceBlockedError as e:
    print(e.verdict)               # "BLOCK", "ESCALATE", "ESCALATE_DENY", ...
    print(e.decision_id)            # GaaS decision ID
    print(e.blocking_policies)      # ["pol_t1_002", ...]
    print(e.governance_proof_token)  # ECDSA-signed proof token ID

Pydantic AI

Install

pip install gaas-pydantic-ai[pydantic-ai]

Govern a Toolset

from pydantic_ai import Agent
from gaas_pydantic_ai import govern_tools, GaaSGovernanceConfig

config = GaaSGovernanceConfig(
    api_url="https://api.gaas.is",
    api_key="gsk_your_key",
    agent_id="my-agent",
)

agent = Agent(
    "anthropic:claude-sonnet-5",
    toolsets=[govern_tools([search_web, send_email], config=config)],
)
result = await agent.run("...")

Wrap an existing toolset with govern_toolset(toolset, config=config). One wrapper governs every tool in the set; tool names and parsed arguments flow into the governance intent.

BLOCK Halts the Run — Natively

from gaas_pydantic_ai import GovernanceBlockedError

try:
    result = await agent.run("wire $250k to the new vendor")
except GovernanceBlockedError as e:
    print(e.verdict)               # "BLOCK", "ESCALATE", "ESCALATE_DENY", ...
    print(e.decision_id)            # GaaS decision ID
    print(e.blocking_policies)      # ["pol_t1_002", ...]
    print(e.governance_proof_token)  # ECDSA-signed proof token ID

Pydantic AI propagates ordinary exceptions out of agent.run() by design, so a blocked action halts the run without any framework workarounds. The adapter deliberately never uses ModelRetry — the model cannot route around a block. Hold-and-poll on ESCALATE is supported via hold_on_escalate=True (approve/modify proceeds; deny/timeout raises ESCALATE_DENY / ESCALATE_TIMEOUT). When GaaS cannot give a decision, the run aborts the same way, with verdict UNEVALUATED, unless fail_open=True (see Framework plugins fail closed).


Microsoft Agent Framework

Install

pip install "gaas-agent-framework[agent-framework]"

Govern Every Tool Call

from agent_framework import Agent
from gaas_agent_framework import GaaSGovernanceConfig, GaaSGovernanceMiddleware

config = GaaSGovernanceConfig(
    api_url="https://api.gaas.is",
    api_key="gsk_your_key",
    agent_id="my-agent",
)

agent = Agent(
    client=chat_client,
    tools=[search_web, send_email],
    middleware=[GaaSGovernanceMiddleware(config)],
)
result = await agent.run("Email the Q3 report to finance@acme.com")

One function middleware governs every tool the agent calls, including local MCP tools: the tool name and arguments, the model's tool-call id and the session id flow into the governance intent. Attach it to the agent, to a single agent.run(), or to a chat client. Tools the model provider runs on its own side (hosted tools) never reach function middleware, so they cannot be governed here.

BLOCK Stops the Run

from gaas_agent_framework import GovernanceBlockedError

try:
    result = await agent.run("Wire $250k to the new vendor")
except GovernanceBlockedError as e:
    print(e.verdict)               # "BLOCK", "ESCALATE", "ESCALATE_DENY", "UNEVALUATED", ...
    print(e.decision_id)            # GaaS decision ID
    print(e.blocking_policies)      # ["pol_t1_001", ...]

GovernanceBlockedError is an Agent Framework MiddlewareFailure, the one exception the framework lets abort a run from function middleware; any other exception would be turned into a tool result and the agent would carry on. To let the agent continue instead, set on_block="inform": the blocked tool still does not run, and the model is told it was not allowed. hold_on_escalate=True waits for a human on ESCALATE, and hold_on_block=True waits for a person to approve a BLOCK, then retries once. When GaaS cannot give a decision, the tool does not run, with verdict UNEVALUATED, unless fail_open=True (see Framework plugins fail closed). Requires agent-framework-core 1.15 or later.


Vercel AI SDK

Install

npm install @governancehq/vercel-ai

ai v7 is a peer dependency.

Govern a ToolSet

import { generateText, stepCountIs } from "ai";
import { governTools, stopOnGovernanceBlock } from "@governancehq/vercel-ai";

const config = { apiKey: "gsk_your_key", agentId: "my-agent" };

const result = await generateText({
  model,
  tools: governTools({ sendEmail, searchWeb }, config),
  stopWhen: [stepCountIs(5), stopOnGovernanceBlock()],
  prompt: "...",
});

Every governed tool call submits a governance intent before executing. Tool metadata (description, inputSchema) is preserved; the ToolSet key becomes the tool name on the intent. Provider-executed tools (no execute) pass through unchanged.

Verdict Semantics

On BLOCK the tool never executes and GovernanceBlockedError is thrown from execute. The AI SDK surfaces tool errors to the model as tool-error parts and the generation continues — each retry is re-governed and lands on the signed audit chain. Add stopOnGovernanceBlock() to stopWhen to halt the agent loop at the blocked step instead; inspect the final step's tool-error part for the decision ID and blocking policies. Hold-and-poll on ESCALATE is supported via holdOnEscalate: true (approve/modify proceeds; deny/timeout throws ESCALATE_DENY / ESCALATE_TIMEOUT). When GaaS cannot give a decision, the tool does not execute either: GovernanceBlockedError with verdict UNEVALUATED (stopOnGovernanceBlock() stops on it too), unless failOpen: true (see Framework plugins fail closed).


MCP Server (Claude Code / Claude Desktop)

@governancehq/mcp is a Model Context Protocol server that gives Claude Code, Claude Desktop, and any MCP client direct access to the GaaS governance pipeline.

Setup — Claude Code

claude mcp add gaas --env GAAS_API_KEY=gsk_your_key -- npx -y @governancehq/mcp

Setup — Claude Desktop / .mcp.json

{
  "mcpServers": {
    "gaas": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@governancehq/mcp"],
      "env": {
        "GAAS_API_URL": "https://api.gaas.is",
        "GAAS_API_KEY": "gsk_your_key"
      }
    }
  }
}

Environment: GAAS_API_KEY (required), GAAS_API_URL (default https://api.gaas.is), GAAS_TIMEOUT_MS (default 240000, enough for a deliberated decision).

Tools

Tool What it does
gaas_submit_intent Submit a governance intent and receive the decision (APPROVE / ESCALATE / BLOCK) with its risk assessment. A live decision's signed proof token is shown by its ID; anyone can check it at /v1/verify/proof/{token_id} without an API key. mode=shadow evaluates without enforcement.
gaas_check_decision Fetch the decision for an intent ID — poll after an ESCALATE verdict.
gaas_query_audit Fetch the full signed audit record: pipeline stages, policy evaluations, hash-chain position.
gaas_validate_governance_files Dry-run validation of .gaas/ governance files against the authmd spec. Never mutates anything.

Resources: governance://current (applied bundle + drift), governance://policy-packs, governance://spec. No write-side policy tools — applying governance bundles from an LLM session is the wrong trust direction.

If gaas_submit_intent gets no decision (network error, timeout, or any HTTP error, including a wrong API key), its error result begins “GaaS could not evaluate this action, so it is not approved. Do not perform it.” An error from gaas_check_decision says the same: the action is not approved.

HTTP Mode

GAAS_API_KEY=gsk_... npx -y @governancehq/mcp --http --port 3917
# MCP endpoint: POST http://localhost:3917/mcp

Common Patterns

Builder Pattern

All three SDKs provide a builder function (build_intent in Python, buildIntent in TypeScript, IntentBuilder in Java) that flattens the nested intent model into a flat argument list. This handles the agent.id, action.type, action.target.identifier nesting so you don't have to construct nested objects manually.

Response Structure

Every SDK method returns a typed response with two fields:

Retrieving Decisions

Python:

decision = await client.get_decision("intent-id")

TypeScript:

const decision = await client.getDecision('intent-id');

Java:

GaaSResponse<GovernanceDecision> decision = client.getDecision("intent-id");

Related Pages