intutic_clawde
Classes
ClawdeBlockedError
class ClawdeBlockedError(
verdict: str,
code: str,
status: int,
message: str,
rule_id: Optional[str] = None,
retry_after_seconds: Optional[int] = None,
)Bases: ClawdeVerdictError
The proxy refused the request: a policy block, a reask, a loop run held for review, a spend cap, a DLP block, or a tool call the model made that the proxy withheld. Never retried — the same request would be refused again. verdict is kill, reask or hold, code the proxy's refusal code, rule_id the rule that decided when the proxy names one (every refusal it answers with a 200 does), and the message its reason.
ClawdeClient
class ClawdeClient(
api_key: str,
base_url: Optional[str] = None,
control_plane_url: Optional[str] = None,
provider: Optional[str] = None,
auto_context: bool = True,
timeout: float = 30.0,
retries: int = 2,
)The data-plane client: chat() sends an OpenAI-format request through the Intutic proxy, which governs it and forwards it to the provider, and raises ClawdeBlockedError when the proxy refuses it. check_budget() and circuit_breaker() add a workspace budget check, and on() registers callbacks for refusal verdicts. For the operator APIs, use ControlPlaneClient.
base_url is the proxy's address: INTUTIC_BASE_URL, then http://localhost:4000. control_plane_url is used by check_budget() and session registration: INTUTIC_CONTROL_PLANE_URL, then Intutic's hosted control plane. auto_context=False turns off context resolution and session registration. timeout is how long one attempt may take, in seconds, and retries how many times a transport failure, a timeout or a 5xx answer is retried.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
listeners | Dict[str, List[Callable[[Dict[str, Any]], None]]] | — |
Methods
ClawdeClient.chat()
def chat(model: str, messages: List[Dict[str, str]], **kwargs: Any) -> Dict[str, Any]Send a chat request through the proxy's /v1/chat/completions route.
Returns the completion with verdict set to "allow" when the proxy let the request through, and upstream set to {"attempts", "fallback_from"} when the proxy's retry layer made more than one upstream call for it. A governance refusal, including one the proxy answers with a 200 and names in x-intutic-refusal (or, on a stream, in its : intutic-refusal line), fires the matching event and raises ClawdeBlockedError, unretried. Transport failures, timeouts and 5xx answers are retried, except a 5xx the proxy already retried upstream; anything else raises ClawdeConnectionError.
ClawdeClient.check_budget()
def check_budget(model: str, estimated_tokens: int) -> Dict[str, Any]ClawdeClient.circuit_breaker()
def circuit_breaker(
tool_name: str,
max_cost_usd: Optional[float] = None,
fail_open: bool = False,
require_budget: bool = False,
) -> Callable[[Callable[[], Any]], Any]require_budget=True runs check_budget() first and refuses to run the function when the workspace has no budget left. max_cost_usd is the deprecated spelling of the same switch: the amount is not compared with anything, because nothing reports a call's cost before it is made.
ClawdeClient.emit()
def emit(event: str, payload: Dict[str, Any]) -> NoneClawdeClient.off()
def off(event: str, callback: Callable[[Dict[str, Any]], None]) -> NoneClawdeClient.on()
def on(event: str, callback: Callable[[Dict[str, Any]], None]) -> NoneClawdeClient.resolve_context()
def resolve_context() -> Dict[str, Any]ClawdeConnectionError
class ClawdeConnectionError(ClawdeError)Raised when the proxy is unreachable, times out, or answers with an error that is not a governance refusal.
ClawdeError
class ClawdeError(Exception)Base exception for intutic-clawde SDK.
ClawdeVerdictError
class ClawdeVerdictError(verdict: str, message: str)Bases: ClawdeError
A governance verdict stopped the call: a proxy refusal (ClawdeBlockedError) or the circuit breaker's budget check.
ControlPlaneClient
class ControlPlaneClient(api_key: str, base_url: Optional[str] = None)The control-plane management client: the operator APIs the intutic CLI exposes, as methods with the same names as the TypeScript SDK's in snake_case. Each method's docstring names the route it calls.
api_key is an Intutic virtual key (vk_...) or a login JWT. base_url is the control plane's address: INTUTIC_CONTROL_PLANE_URL, then Intutic's hosted control plane. A call that fails raises ClawdeConnectionError with the method, path, status and the control plane's answer.
Methods
ControlPlaneClient.add_policy_source()
def add_policy_source(source: PolicySourceInput) -> Dict[str, Any]POST /api/v1/connectors
ControlPlaneClient.adjudicate_finding()
def adjudicate_finding(
finding_id: str,
outcome: str,
note: Optional[str] = None,
) -> FindingAdjudicationPOST /api/v1/findings/:findingId/adjudicate -- outcome is "TRUE_POSITIVE" or "FALSE_POSITIVE"; the ruling is recorded under the key's member.
ControlPlaneClient.approve_decision()
def approve_decision(hold_id: str, reason: Optional[str] = None) -> DecisionReviewResultPOST /api/v1/decisions/:holdId/review (OWNER, ADMIN, EM) -- approve a held call.
ControlPlaneClient.approve_guardrail_shadow()
def approve_guardrail_shadow(guardrail_id: str) -> GuardrailTransitionResultPOST .../guardrails/:guardrailId/approve-shadow -- into SHADOW: distributed as warn, measuring, enforcing nothing.
ControlPlaneClient.approve_loop_run()
def approve_loop_run(
loop_run_id: str,
note: Optional[str] = None,
) -> LoopRunActionResultPOST /api/v1/loops/:loopRunId/review (OWNER, ADMIN, EM) -- approve a run held for review; it is ACTIVE again.
ControlPlaneClient.approve_mcp_server()
def approve_mcp_server(server_id: str) -> McpServerDecisionPOST /api/v1/mcp/servers/:serverId/status -- approve a server (OWNER, ADMIN).
ControlPlaneClient.assign_org_gateway()
def assign_org_gateway(org_id: str, gateway_id: Optional[str]) -> Dict[str, Any]PATCH /api/v1/orgs/:orgId/gateway -- pass None to clear the org default.
ControlPlaneClient.assign_workspace_gateway()
def assign_workspace_gateway(gateway_id: Optional[str]) -> Dict[str, Any]PATCH /api/v1/workspace/gateway -- pass None to clear the override.
ControlPlaneClient.attenuate_key()
def attenuate_key(
parent_key_id: str,
requested_caps: List[str],
ttl_seconds: Optional[int] = None,
) -> AttenuationResultPOST /api/v1/attenuate -- a child of one of your keys with narrower scopes: each of requested_caps is an API key scope (gateway:use, traces:read, ...) the parent grants (ttl_seconds 60 to 86400). The child key is in the response once and never stored.
ControlPlaneClient.block_mcp_server()
def block_mcp_server(server_id: str) -> McpServerDecisionPOST /api/v1/mcp/servers/:serverId/status -- block a server (OWNER, ADMIN).
ControlPlaneClient.check_domain_verification()
def check_domain_verification(verification_id: str) -> Dict[str, Any]GET /api/v1/domain-verification/:id -- re-checks DNS for the TXT record. Safe to call repeatedly; performs a fresh lookup every call.
ControlPlaneClient.collect_evidence()
def collect_evidence(
period_start: Optional[str] = None,
period_end: Optional[str] = None,
) -> EvidenceCollectResultPOST /api/v1/compliance/soc2-collect (OWNER, ADMIN) -- run a fresh evidence collection and seal it. The period bounds are ISO 8601.
ControlPlaneClient.complete_loop_run()
def complete_loop_run(
loop_run_id: str,
outcome: Optional[str] = None,
) -> LoopRunActionResultPOST /api/v1/loops/:loopRunId/complete -- outcome ("SUCCEEDED" or "FAILED") records how the run ended.
ControlPlaneClient.create_guardrail()
def create_guardrail(guardrail: AuthoredGuardrailInput) -> AuthoredGuardrailWriteResultPOST /api/v1/policy-guardrails/guardrails (OWNER, ADMIN) -- an authored guardrail, created PROPOSED. An IR a check refuses is a 400 naming the check.
ControlPlaneClient.create_notification_rule()
def create_notification_rule(rule: NotificationRuleInput) -> NotificationRulePOST /api/v1/notifications/rules. A webhook rule comes back with its signingSecret, which is not shown again.
ControlPlaneClient.create_org()
def create_org(
org_name: str,
domain: str,
verification_id: str,
region: Optional[str] = None,
) -> Dict[str, Any]POST /api/v1/orgs -- creates a real org from an already-authenticated caller. Requires a 'verified', unconsumed domain verification (see start_domain_verification); the verification is consumed atomically inside the org-insert transaction, so it backs exactly this one org. region picks the managed gateway cell's placement ('us', 'eu', ...); omitted, the deployment's home region applies. Server-validated.
ControlPlaneClient.create_org_sop()
def create_org_sop(sop: SopInput) -> Dict[str, Any]POST /api/v1/workspace/org-sops -- a mandatory org-wide floor (OWNER, ADMIN of a workspace in the org); title, markdown_content and an optional risk_tier.
ControlPlaneClient.create_siem_destination()
def create_siem_destination(destination: SiemDestinationInput) -> SiemDestinationPOST /api/v1/siem/destinations (OWNER, ADMIN). The control plane refuses a destination that points at an internal address. A webhook destination comes back with its signingSecret, which is not shown again.
ControlPlaneClient.create_sop()
def create_sop(sop: SopInput) -> SopCreateResultPOST /api/v1/sops -- one workspace SOP; markdown_content without front matter.
ControlPlaneClient.create_team()
def create_team(org_id: str, name: str) -> Dict[str, Any]POST /api/v1/orgs/:orgId/teams
ControlPlaneClient.create_workspace()
def create_workspace(team_id: str, name: str) -> Dict[str, Any]POST /api/v1/teams/:teamId/workspaces
ControlPlaneClient.delete_guardrail()
def delete_guardrail(guardrail_id: str) -> GuardrailTransitionResultDELETE /api/v1/policy-guardrails/guardrails/:guardrailId (OWNER, ADMIN) -- retires it; its history is kept.
ControlPlaneClient.delete_notification_rule()
def delete_notification_rule(rule_id: str) -> NoneDELETE /api/v1/notifications/rules/:ruleId
ControlPlaneClient.delete_org_sop()
def delete_org_sop(org_sop_id: str) -> NoneDELETE /api/v1/workspace/org-sops/:orgSopId -- answers 404 both for no such SOP and for a member without admin access.
ControlPlaneClient.delete_siem_destination()
def delete_siem_destination(destination_id: str) -> NoneDELETE /api/v1/siem/destinations/:destinationId (OWNER, ADMIN) -- deactivates it; update_siem_destination(id, {"isActive": True}) turns it back on.
ControlPlaneClient.disable_mcp_tool()
def disable_mcp_tool(server_id: str, tool: str) -> McpServerDecisionPOST /api/v1/mcp/servers/:serverId/tools -- switch one tool off (OWNER, ADMIN).
ControlPlaneClient.download_evidence()
def download_evidence(run_id: str) -> bytesGET /api/v1/compliance/soc2-export/:runId (OWNER, ADMIN) -- a stored archive, as the file. Check it with verify_evidence_archive.
ControlPlaneClient.download_framework_coverage()
def download_framework_coverage(framework_id: str, format: str) -> bytesGET /api/v1/compliance/frameworks/:frameworkId/coverage?format= -- the report as a file: format is "json", "md", "csv" or "pdf". Unsigned: the signed copies are the ones sealed in the evidence archive.
ControlPlaneClient.download_inventory_harnesses_csv()
def download_inventory_harnesses_csv(
status: Optional[str] = None,
harness: Optional[str] = None,
device: Optional[str] = None,
search: Optional[str] = None,
) -> bytesGET /api/v1/inventory/harnesses?format=csv -- the CSV the dashboard downloads.
ControlPlaneClient.download_inventory_mcp_servers_csv()
def download_inventory_mcp_servers_csv(
status: Optional[str] = None,
harness: Optional[str] = None,
device: Optional[str] = None,
search: Optional[str] = None,
) -> bytesGET /api/v1/inventory/mcp-servers?format=csv
ControlPlaneClient.enable_mcp_tool()
def enable_mcp_tool(server_id: str, tool: str) -> McpServerDecisionPOST /api/v1/mcp/servers/:serverId/tools -- switch one tool on (OWNER, ADMIN).
ControlPlaneClient.extract_policy_document()
def extract_policy_document(doc_id: str, llm: bool = True) -> Dict[str, Any]POST /api/v1/policy-guardrails/documents/:docId/extract -- propose guardrails from the document's clauses. llm=False runs only the front-matter lift, with no model call.
ControlPlaneClient.get_attenuation_chain()
def get_attenuation_chain(chain_id: str) -> List[Dict[str, Any]]GET /api/v1/attenuate/chain/:chainId (OWNER, ADMIN) -- the delegation lineage.
ControlPlaneClient.get_branch_usage()
def get_branch_usage(period: str = 'monthly') -> BranchUsageResponseGET /api/v1/usage/branches
ControlPlaneClient.get_commit_usage()
def get_commit_usage(period: str = 'monthly') -> CommitUsageResponseGET /api/v1/usage/commits
ControlPlaneClient.get_finding_stats()
def get_finding_stats() -> FindingStatsGET /api/v1/findings/stats -- false-positive rate per detector, over adjudicated findings.
ControlPlaneClient.get_framework_coverage()
def get_framework_coverage(framework_id: str) -> FrameworkCoverageGET /api/v1/compliance/frameworks/:frameworkId/coverage -- eu_ai_act, iso_42001, nist_ai_rmf or mitre_atlas.
ControlPlaneClient.get_gate_liveness()
def get_gate_liveness() -> GateLivenessGET /api/v1/governance/gate-liveness (OWNER, ADMIN, EM) -- whether each installed harness's gate is reporting.
ControlPlaneClient.get_gateway_config()
def get_gateway_config(gateway_id: str) -> Dict[str, Any]GET /api/v1/gateways/:id/config -- the flags set on the gateway (requireVk, requireProvisionedKey) and configVersion, as the gateway pulls them. A flag never set is absent: the gateway runs its deployment's own value.
ControlPlaneClient.get_gateway_status()
def get_gateway_status(gateway_id: str) -> GatewayStatusGET /api/v1/gateways/:id/status
ControlPlaneClient.get_github_webhook()
def get_github_webhook() -> GitHubWebhookInfoGET /api/v1/integrations/github/webhook (OWNER, ADMIN) -- the payload URL; never the secret.
ControlPlaneClient.get_guardrail()
def get_guardrail(guardrail_id: str) -> Dict[str, Any]GET /api/v1/policy-guardrails/guardrails/:guardrailId
ControlPlaneClient.get_guardrail_coverage()
def get_guardrail_coverage(token: str) -> Dict[str, Any]GET /api/v1/policy-guardrails/coverage?token= -- the passages that mention a tool or action, and the guardrails on them.
ControlPlaneClient.get_guardrail_impact()
def get_guardrail_impact(
doc_id: Optional[str] = None,
passage_id: Optional[str] = None,
) -> Dict[str, Any]GET /api/v1/policy-guardrails/impact -- what a change to a document or a passage reaches. Name one of them.
ControlPlaneClient.get_guardrail_readiness()
def get_guardrail_readiness(guardrail_id: str) -> Dict[str, Any]GET /api/v1/policy-guardrails/guardrails/:guardrailId/readiness -- whether a SHADOW guardrail's evidence clears promotion.
ControlPlaneClient.get_incident()
def get_incident(incident_id: str) -> IncidentGET /api/v1/incidents/:incidentId
ControlPlaneClient.get_integrity_chain()
def get_integrity_chain() -> IntegrityChainGET /api/v1/integrity/chain -- the root chain walk; intact: False lists the breaks (the control plane answers a break with a 409, which is still the walk).
ControlPlaneClient.get_integrity_config_chain()
def get_integrity_config_chain() -> ConfigChainGET /api/v1/integrity/config-chain -- the harness config snapshot chain walk.
ControlPlaneClient.get_integrity_root()
def get_integrity_root(root_id: str) -> IntegrityRootDetailGET /api/v1/integrity/roots/:rootId -- the root, its signature and its leaves.
ControlPlaneClient.get_inventory_summary()
def get_inventory_summary() -> InventorySummaryGET /api/v1/inventory/summary
ControlPlaneClient.get_loop_run()
def get_loop_run(loop_run_id: str) -> Dict[str, Any]GET /api/v1/loops/:loopRunId
ControlPlaneClient.get_member_usage()
def get_member_usage(period: str = 'monthly') -> MemberUsageResponseGET /api/v1/usage/members -- period is "daily" or "monthly".
ControlPlaneClient.get_mirror_adoption_report()
def get_mirror_adoption_report(candidate_model: str) -> Dict[str, Any]GET /api/v1/routing/mirror-adoption-report -- how a mirror-tested candidate model compared. Reported only; changes nothing.
ControlPlaneClient.get_policy_document()
def get_policy_document(doc_id: str) -> Dict[str, Any]GET /api/v1/policy-guardrails/documents/:docId -- passages, clauses and extraction runs.
ControlPlaneClient.get_pull_request_usage()
def get_pull_request_usage(period: str = 'monthly') -> PullRequestUsageResponseGET /api/v1/usage/pull-requests
ControlPlaneClient.get_response_echo_report()
def get_response_echo_report(
since: Optional[str] = None,
until: Optional[str] = None,
) -> ResponseEchoReportGET /api/v1/findings/response-echo/report -- the bounds are ISO 8601.
ControlPlaneClient.get_rule_candidate_source()
def get_rule_candidate_source(candidate_id: str) -> RuleCandidateSourceGET /api/v1/rule-candidates/:candidateId/source -- the source of record a candidate's bundle must be compiled from. A source that does not hash to the sourceSha256 served with it is refused (ClawdeConnectionError): something between the control plane and this process changed it.
ControlPlaneClient.get_siem_destination()
def get_siem_destination(destination_id: str) -> SiemDestinationGET /api/v1/siem/destinations/:destinationId
ControlPlaneClient.get_signing_keys()
def get_signing_keys() -> SigningJwksGET /.well-known/intutic-trace-signing.json -- the published signing keys, fetched without credentials. verify_evidence_archive takes them.
ControlPlaneClient.get_sop()
def get_sop(sop_id: str) -> Dict[str, Any]GET /api/v1/sops/:sopId
ControlPlaneClient.get_team_usage()
def get_team_usage(period: str = 'monthly') -> TeamUsageResponseGET /api/v1/usage/teams -- per SCIM group.
ControlPlaneClient.get_trace()
def get_trace(trace_id: str) -> Dict[str, Any]GET /api/v1/traces/:traceId
ControlPlaneClient.get_workspace_settings()
def get_workspace_settings() -> Dict[str, Any]GET /api/v1/workspace/settings -- every setting, resolved with its default.
ControlPlaneClient.kill_loop_run()
def kill_loop_run(loop_run_id: str) -> LoopRunActionResultPOST /api/v1/loops/:loopRunId/kill
ControlPlaneClient.list_findings()
def list_findings(
unadjudicated: bool = False,
detector_id: Optional[str] = None,
limit: Optional[int] = None,
) -> List[Dict[str, Any]]GET /api/v1/findings -- detector findings, newest first; unadjudicated=True keeps only the ones nobody has ruled on.
ControlPlaneClient.list_gateways()
def list_gateways() -> List[Dict[str, Any]]GET /api/v1/gateways
ControlPlaneClient.list_guardrail_conflicts()
def list_guardrail_conflicts() -> List[Dict[str, Any]]GET /api/v1/policy-guardrails/conflicts -- live guardrails and front-matter rules that contradict each other.
ControlPlaneClient.list_guardrail_duplicates()
def list_guardrail_duplicates(min_jaccard: Optional[float] = None) -> Dict[str, Any]GET /api/v1/policy-guardrails/duplicates -- overlapping passages and rules cited more than once. min_jaccard is 0 to 1.
ControlPlaneClient.list_guardrails()
def list_guardrails(
status: Optional[str] = None,
target: Optional[str] = None,
provenance: Optional[str] = None,
doc_id: Optional[str] = None,
limit: Optional[int] = None,
) -> List[Dict[str, Any]]GET /api/v1/policy-guardrails/guardrails
ControlPlaneClient.list_incidents()
def list_incidents(
status: Optional[str] = None,
severity: Optional[str] = None,
type: Optional[str] = None,
page: Optional[int] = None,
limit: Optional[int] = None,
) -> IncidentListGET /api/v1/incidents (OWNER, ADMIN, EM) -- ranked by review priority. status is OPEN, RESOLVED or AUTO_RESOLVED; severity CRITICAL, HIGH, MEDIUM or LOW; type an anomaly category such as SCOPE_VIOLATION, or WASM_RULE_REFUSED or SYSTEM_ANOMALY, and the control plane refuses one it does not know. limit is 1 to 100 (default 20).
ControlPlaneClient.list_integrity_roots()
def list_integrity_roots(loop_run_id: Optional[str] = None) -> IntegrityRootListGET /api/v1/integrity/roots -- the sealed trace roots.
ControlPlaneClient.list_inventory_devices()
def list_inventory_devices() -> List[Dict[str, Any]]GET /api/v1/inventory/devices -- one row per machine.
ControlPlaneClient.list_inventory_disconnects()
def list_inventory_disconnects(limit: Optional[int] = None) -> List[DeviceDisconnect]GET /api/v1/inventory/disconnects -- machines that ran intutic disconnect, newest first (default 100, at most 500).
ControlPlaneClient.list_inventory_harnesses()
def list_inventory_harnesses(
status: Optional[str] = None,
harness: Optional[str] = None,
device: Optional[str] = None,
search: Optional[str] = None,
) -> List[Dict[str, Any]]GET /api/v1/inventory/harnesses -- device is a device id; search matches the harness name and the machine's hostname.
ControlPlaneClient.list_inventory_mcp_servers()
def list_inventory_mcp_servers(
status: Optional[str] = None,
harness: Optional[str] = None,
device: Optional[str] = None,
search: Optional[str] = None,
) -> List[Dict[str, Any]]GET /api/v1/inventory/mcp-servers
ControlPlaneClient.list_inventory_skills()
def list_inventory_skills(
device: Optional[str] = None,
search: Optional[str] = None,
) -> List[Dict[str, Any]]GET /api/v1/inventory/skills
ControlPlaneClient.list_loop_runs()
def list_loop_runs() -> List[Dict[str, Any]]GET /api/v1/loops
ControlPlaneClient.list_mcp_servers()
def list_mcp_servers() -> McpServerListGET /api/v1/mcp/servers -- every server the workspace's MCP proxies have reported, and the registry-wide policy.
ControlPlaneClient.list_notification_rules()
def list_notification_rules() -> List[NotificationRule]GET /api/v1/notifications/rules
ControlPlaneClient.list_org_sops()
def list_org_sops() -> List[Dict[str, Any]]GET /api/v1/workspace/org-sops -- the org-wide SOP floors of the key's org.
ControlPlaneClient.list_policy_documents()
def list_policy_documents() -> List[Dict[str, Any]]GET /api/v1/policy-guardrails/documents
ControlPlaneClient.list_policy_sources()
def list_policy_sources() -> List[Dict[str, Any]]GET /api/v1/connectors -- the workspace's policy sources (Notion, Confluence, GitHub, Google Drive).
ControlPlaneClient.list_provider_credentials()
def list_provider_credentials() -> List[Dict[str, Any]]GET /api/v1/workspace/provider-credentials
ControlPlaneClient.list_provider_models()
def list_provider_models(provider: str) -> Dict[str, Any]GET /api/v1/workspace/provider-credentials/:provider/models -- the models the stored key can reach, as the provider listed them when the key was last checked: {"provider", "models", "checkedAt"}, with models and checkedAt None until a check has recorded a list.
ControlPlaneClient.list_siem_destinations()
def list_siem_destinations() -> SiemDestinationListGET /api/v1/siem/destinations -- the destinations and the event sources they can receive.
ControlPlaneClient.list_siem_sources()
def list_siem_sources() -> SiemSourcesThe event sources a destination can receive; defaults are the ones an empty sourceTables means.
ControlPlaneClient.list_sops()
def list_sops(limit: Optional[int] = None) -> List[Dict[str, Any]]GET /api/v1/sops
ControlPlaneClient.list_team_workspaces()
def list_team_workspaces(team_id: str) -> List[Dict[str, Any]]GET /api/v1/teams/:teamId/workspaces
ControlPlaneClient.list_teams()
def list_teams(org_id: str) -> List[Dict[str, Any]]GET /api/v1/orgs/:orgId/teams
ControlPlaneClient.list_traces()
def list_traces(
limit: Optional[int] = None,
offset: Optional[int] = None,
since: Optional[str] = None,
enforcement: Optional[str] = None,
model: Optional[str] = None,
) -> TraceListGET /api/v1/traces -- newest first. limit is 1 to 100 (default 20); since is ISO 8601 or a relative duration such as "24h".
ControlPlaneClient.predict_cost()
def predict_cost(params: PredictCostParams) -> CostPredictionPOST /api/v1/predict-cost -- estimate a call's cost before it runs. Size the input with inputTokenCount or inputText.
ControlPlaneClient.promote_guardrail()
def promote_guardrail(
guardrail_id: str,
acknowledge_no_traffic: bool = False,
) -> GuardrailTransitionResultPOST .../guardrails/:guardrailId/promote -- into ENFORCING. Refused with a 409 until the shadow evidence clears the thresholds; a guardrail that never fired needs acknowledge_no_traffic.
ControlPlaneClient.recompute_integrity_root()
def recompute_integrity_root(root_id: str) -> IntegrityRecomputePOST /api/v1/integrity/roots/:rootId/recompute -- re-derive the root from the stored traces.
ControlPlaneClient.reconfirm_guardrail()
def reconfirm_guardrail(guardrail_id: str) -> GuardrailTransitionResultPOST .../guardrails/:guardrailId/reconfirm -- re-confirm a stale citation against a live passage.
ControlPlaneClient.refresh_pull_request_usage()
def refresh_pull_request_usage() -> PullRequestRefreshResultPOST /api/v1/usage/pull-requests/refresh -- look the branches up on GitHub now (OWNER, ADMIN, EM).
ControlPlaneClient.register_gateway()
def register_gateway(name: str, deployment_target: str) -> Dict[str, Any]POST /api/v1/gateways
ControlPlaneClient.reject_decision()
def reject_decision(hold_id: str, reason: Optional[str] = None) -> DecisionReviewResultPOST /api/v1/decisions/:holdId/review (OWNER, ADMIN, EM) -- reject a held call.
ControlPlaneClient.reject_guardrail()
def reject_guardrail(guardrail_id: str, reason: str) -> GuardrailTransitionResultPOST .../guardrails/:guardrailId/reject -- recorded with the reason.
ControlPlaneClient.reject_loop_run()
def reject_loop_run(loop_run_id: str, note: Optional[str] = None) -> LoopRunActionResultPOST /api/v1/loops/:loopRunId/review (OWNER, ADMIN, EM) -- reject a run held for review; it is KILLED.
ControlPlaneClient.replay_guardrail()
def replay_guardrail(guardrail_id: str) -> GuardrailReplayPOST .../guardrails/:guardrailId/replay -- what the guardrail would have done on captured traffic.
ControlPlaneClient.replay_wasm_rule()
def replay_wasm_rule(
rule_id: str,
limit: Optional[int] = None,
since: Optional[str] = None,
) -> WasmReplayResultPOST /api/v1/wasm-rules/:ruleId/replay -- what a rule would have done on sampled traffic, without enforcing.
ControlPlaneClient.reset_mcp_server()
def reset_mcp_server(server_id: str) -> McpServerDecisionPOST /api/v1/mcp/servers/:serverId/status -- return a server to the approval queue (OWNER, ADMIN).
ControlPlaneClient.resolve_gateway()
def resolve_gateway() -> Dict[str, Any]GET /api/v1/workspace/gateway-resolution
ControlPlaneClient.retire_guardrail()
def retire_guardrail(guardrail_id: str) -> GuardrailTransitionResultPOST .../guardrails/:guardrailId/retire -- gone from every rule endpoint on the next poll.
ControlPlaneClient.revoke_gateway()
def revoke_gateway(gateway_id: str, reason: Optional[str] = None) -> NoneDELETE /api/v1/gateways/:id
ControlPlaneClient.rotate_gateway_token()
def rotate_gateway_token(gateway_id: str) -> Dict[str, Any]POST /api/v1/gateways/:id/rotate
ControlPlaneClient.rotate_github_webhook_secret()
def rotate_github_webhook_secret() -> GitHubWebhookSecretPOST /api/v1/integrations/github/webhook/secret (OWNER, ADMIN) -- make the webhook, or replace its secret. Deliveries signed with an earlier secret are refused from then on.
ControlPlaneClient.rotate_notification_rule_secret()
def rotate_notification_rule_secret(rule_id: str) -> NotificationSecretRotationPOST /api/v1/notifications/rules/:ruleId/signing-secret -- deliveries are signed with the new secret from now on.
ControlPlaneClient.rotate_siem_destination_secret()
def rotate_siem_destination_secret(destination_id: str) -> SiemSecretRotationPOST /api/v1/siem/destinations/:destinationId/signing-secret (OWNER, ADMIN)
ControlPlaneClient.search_policy_passages()
def search_policy_passages(text: str) -> Dict[str, Any]GET /api/v1/policy-guardrails/search?q= -- full-text search over live passages, best match first.
ControlPlaneClient.set_gateway_config()
def set_gateway_config(
gateway_id: str,
require_vk: Optional[bool] = None,
require_provisioned_key: Optional[bool] = None,
) -> Dict[str, Any]PATCH /api/v1/gateways/:id/config
ControlPlaneClient.set_provider_credential()
def set_provider_credential(provider: str, fields: Dict[str, str]) -> Dict[str, Any]PUT /api/v1/workspace/provider-credentials/:provider
ControlPlaneClient.signup_org()
def signup_org(
email: str,
password: str,
name: str,
org_name: str,
*,
accept_terms: bool,
) -> Dict[str, Any]POST /api/v1/auth/signup/org -- unauthenticated, creates the calling user.
accept_terms=True is required: the caller is 18 or older and agrees to the Terms and the Privacy Policy. Without it the call fails with 400 TERMS_NOT_ACCEPTED.
Closed by default in production (INTUTIC_PUBLIC_ORG_SIGNUP, off unless a deployment has built its own anonymous domain-verification story): creating a real org auto-provisions a real managed gateway cell, so org creation now requires DNS domain-ownership proof, and an anonymous caller has no session to own a verification attempt against. Prefer start_domain_verification + check_domain_verification + create_org with an already-authenticated api_key -- the same flow the CLI's intutic org create and the dashboard's "Create Organization" modal use.
ControlPlaneClient.start_domain_verification()
def start_domain_verification(domain: str) -> Dict[str, Any]POST /api/v1/domain-verification/start -- mints a DNS TXT-record verification token for domain. Publish a TXT record at txtRecordName with value txtRecordValue, then poll check_domain_verification until status is 'verified'.
ControlPlaneClient.start_loop_run()
def start_loop_run(
name: str,
budget_limit_usd: Optional[Union[float, str]] = None,
sops: Optional[List[str]] = None,
auto_judge: Optional[bool] = None,
) -> Dict[str, Any]POST /api/v1/loops/start -- register a loop run. Send its id with the run's requests (INTUTIC_LOOP_RUN_ID, as intutic loop exec sets it) so the proxy files them under it.
ControlPlaneClient.sync_policy_source()
def sync_policy_source(connector_id: str) -> PolicySourceSyncResultPOST /api/v1/connectors/:connectorId/sync
ControlPlaneClient.unset_provider_credential()
def unset_provider_credential(provider: str) -> NoneDELETE /api/v1/workspace/provider-credentials/:provider
ControlPlaneClient.update_guardrail()
def update_guardrail(
guardrail_id: str,
changes: AuthoredGuardrailInput,
) -> AuthoredGuardrailWriteResultPUT /api/v1/policy-guardrails/guardrails/:guardrailId (OWNER, ADMIN). A changed IR creates the next version (forked), PROPOSED with no evidence; a name or description edit keeps the evidence.
ControlPlaneClient.update_notification_rule()
def update_notification_rule(
rule_id: str,
changes: NotificationRuleInput,
) -> NotificationRulePUT /api/v1/notifications/rules/:ruleId -- only the fields given change. A switch to the webhook channel comes back with the new signingSecret.
ControlPlaneClient.update_siem_destination()
def update_siem_destination(
destination_id: str,
changes: SiemDestinationInput,
) -> SiemDestinationPUT /api/v1/siem/destinations/:destinationId (OWNER, ADMIN) -- only the fields given change.
ControlPlaneClient.update_workspace_settings()
def update_workspace_settings(settings: Dict[str, Any]) -> Dict[str, Any]PUT /api/v1/workspace/settings -- the route intutic settings set calls.
Only the keys given change; the rest are kept (featureFlags merges one level deeper, so one flag does not switch the others off). The control plane is the authority on which keys exist and what each accepts: an unknown key or a bad value is refused with a 400 naming it, a setting the workspace's plan does not include (the group policy below Biz Org) with a 403 "Upgrade required", and a member below OWNER or ADMIN with a 403. Each refusal raises ClawdeConnectionError carrying the server's answer.
ControlPlaneClient.upload_rule_candidate_bundle()
def upload_rule_candidate_bundle(
candidate_id: str,
wasm: bytes,
source_sha256: str,
file_name: str = 'rule.wasm',
) -> BundleUploadResultPOST /api/v1/rule-candidates/:candidateId/bundle (OWNER, ADMIN) -- upload a compiled rule with the hash of the source it was compiled from. The control plane runs its gates; accepted says whether the rule went into shadow, and gates why.
ControlPlaneClient.whoami()
def whoami() -> Dict[str, Any]GET /api/v1/auth/me
Gate
class Gate(cfg: GateConfig, client: Optional[GateClient] = None)The pre-execution gate. guard() evaluates one tool call against the local tiers (SSO group policy, policy snapshot, MCP servers, image integrity) and, with a GateClient, the control-plane tiers (SOP rules, hook gate), and raises IntuticGateRefusal to refuse it. Without a client only the local tiers run.
Methods
Gate.guard()
def guard(tool_name: str, tool_input: dict) -> NoneRaise IntuticGateRefusal if this call must not run. Return to allow.
Under a chaos run's plan, the call is reported to the run's events file whatever the tiers decide, and a call they allowed can still be refused by the fault that is due (CHAOS_FAULT, or IntuticGateHold for a held one). A plan that is set but unreadable refuses every call: an experiment run without the faults it asked for reports a resilience it never measured.
Gate.policy()
def policy() -> dictLoad the image allowlist.
A missing or malformed allowlist is NOT treated as "allow everything". An empty policy with require_digest still refuses every unpinned image, which is the safe direction; a policy we cannot read at all raises, and the caller turns that into a block.
Gate.snapshot()
def snapshot() -> snapshot.SnapshotGate.sop_rules()
def sop_rules() -> listActive SOP rules, fetched once per process.
Deliberately not refreshed on a timer the way policy.ts does: a rule set that changes mid-run makes a verdict depend on when the fetch landed. Construct a new Gate (or clear _sop_rules) to refresh.
GateClient
class GateClient(
base_url: Optional[str] = None,
api_key: str = '',
workspace_id: str = '',
session_id: str = '',
harness: str = 'langgraph',
fail_closed: bool = True,
timeout: float = 2.0,
transport: Optional[Transport] = None,
get_transport: Optional[GetTransport] = None,
)Control-plane client for the pre-execution gate.
base_url is the control plane's address: INTUTIC_CONTROL_PLANE_URL, then Intutic's hosted control plane. Without an api_key the client is local_only. harness defaults to "langgraph" and is sent as harnessType on every gate call and event, so incidents attribute to the right adapter. Override it when wrapping a different framework. fail_closed blocks a call when the hook gate gives no verdict (the control plane cannot be reached, answers without one, or answers GOVERNANCE_UNAVAILABLE); False lets such a call go ahead. timeout is how long one control-plane request may take, in seconds. transport and get_transport replace the HTTP POST and GET calls, for tests.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
local_only | bool | — | True when there is no API key. Such a client cannot authenticate, so it sends nothing: the control-plane tiers (SOP register, hook gate) are skipped, events and holds are not reported, and the local tiers decide. |
Methods
GateClient.approved_bypasses()
def approved_bypasses() -> Optional[list]The workspace's approved bypasses (GET /api/v1/decisions/approved-bypasses), or None when they cannot be read. Never raises: a hold that cannot be checked for an approval stays held.
GateClient.emit()
def emit(
event: str,
tool_name: str,
reason: str = '',
tool_input: Optional[dict] = None,
incident_id: Optional[str] = None,
file_path: Optional[str] = None,
gate_decision_id: Optional[str] = None,
) -> boolFire-and-forget telemetry. Never raises.
gate_decision_id is the hook gate's decisionId for the call this event reports: the control plane then files the decision's incident once and does not export it to SIEM a second time.
A tool_blocked event creates a governance_incidents row whose description embeds our reason verbatim:
[Hook Gate] Tool "shell" blocked by local SOP enforcement. Reason: <reason> Harness: langgraph
That string is what appears on the incidents dashboard.
GateClient.from_env()
@classmethod
def from_env(
session_id: Optional[str] = None,
harness: str = 'langgraph',
fail_closed: bool = True,
) -> 'GateClient'Build a client from the environment, falling back to CLI credentials.
Reading ~/.intutic/credentials.json means the gate works with whatever workspace intutic login already bound, instead of duplicating the key. With no key in either place the client is local_only.
GateClient.hook_gate()
def hook_gate(tool_name: str, tool_input: dict) -> GateResponseSynchronous allow/deny for one tool call.
Worth being precise about: the endpoint matches ~19 DLP regexes against the serialised toolInput, plus SOP rules. It is defence in depth on top of the local tiers, not a replacement for them.
When there is no verdict (the control plane cannot be reached, answers non-2xx or without a boolean allowed, or answers GOVERNANCE_UNAVAILABLE because its own checks could not run), the verdict is decided by fail_closed: True (the default) reports BLOCK, because a client with no verdict has nothing to defer to; False reports allow. A 400 INVALID_REQUEST is a BLOCK whatever fail_closed says. A shadow-mode workspace's {allowed: true, degraded: true} is an allow.
A local_only client makes no call and answers allow with code LOCAL_ONLY, whatever fail_closed says: with no key there is no control-plane tier to fail, only local ones, which have already run.
GateClient.judge_finalize()
def judge_finalize(
full_content: str,
monitored_model: str,
personal_sops: Optional[list] = None,
) -> dictGrade generated text against the workspace's active SOP.
POST /api/v1/judge/finalize. Note that plain /api/v1/judge does NOT exist — it is a JWT-bypass prefix in PUBLIC_ROUTES, not a handler, and returns 404.
The endpoint interpolates the active SOP's markdown verbatim into the judge's system prompt, which is what makes it usable as a general "does this text contradict this document" check. The SOP must be VALIDATED and active; only one is ever loaded (newest by updatedAt).
Returns {verdict, triggered, personalTriggered, correctionSummary, independent}. verdict is 'PASS' | 'TRIGGERED' | 'REVIEW' from the control plane, or 'UNAVAILABLE' — set by the control plane on its own failures (it returns HTTP 503, which raises in _post and lands in the except below) and by this method on transport errors. Branch on verdict, and treat only 'PASS' as clean:
- 'TRIGGERED': a violation was found.
- 'REVIEW': the response was held for human review. The typed judge was unsure, or did not answer in time (the judge GPU was busy). No org verdict exists yet:
triggeredis False because nothing was decided, not because the response is clean. A fail-closed caller must treat REVIEW as not clean. The control plane records the interim verdict and the reviewer's ruling on its side. - 'UNAVAILABLE' (or any other value): NO grading happened.
This cannot block. lib/monitorModel.ts is explicit: no LLM verdict blocks a request; the KILL sites in the proxy are deterministic. The caller must branch on the verdict itself.
History worth keeping: the endpoint used to fail OPEN — every internal failure (LiteLLM down, 30s timeout, unparsable verdict) returned HTTP 200 {triggered: false}, indistinguishable from a clean pass, and this client's except-branch only caught transport errors. The control plane now 503s with verdict:'UNAVAILABLE' on every failure mode, which routes them all through the except below — so a fail-closed caller finally covers what it always claimed to.
GateClient.record_hold()
def record_hold(hold: Dict[str, Any]) -> boolRecord one hold in the review queue (POST /api/v1/decisions), the way the hook gates' records reach it. Never raises; False when the control plane did not accept it, so the refusal can say there is nothing to approve yet.
GateConfig
@dataclass
class GateConfig(
repo_root: str = '.',
workspace_id: str = '',
allowlist_path: str = '.intutic/image-allowlist.json',
enforce: bool = True,
use_hook_gate: bool = True,
use_sop_rules: bool = True,
)How a Gate evaluates calls. Every field has a default.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
allowlist_path | str | '.intutic/image-allowlist.json' | The image allowlist, relative to repo_root unless absolute. |
enforce | bool | True | False turns Gate.guard() into a no-op. |
repo_root | str | '.' | Where relative manifest and allowlist paths resolve from. |
use_hook_gate | bool | True | Call the control plane's hook gate (needs a client). |
use_sop_rules | bool | True | Fetch and apply the workspace's SOP rules (needs a client). |
workspace_id | str | '' | Compared with the workspace the policy snapshot was compiled for, and scopes the SOP-rule fetch. |
Methods
GateConfig.allowlist_abs()
def allowlist_abs() -> strGateResponse
@dataclass
class GateResponse(
allowed: bool,
reason: str = '',
incident_id: Optional[str] = None,
reached: bool = True,
code: Optional[str] = None,
decision_id: Optional[str] = None,
)Attributes
| Name | Type | Default | Description |
|---|---|---|---|
allowed | bool | — | |
code | Optional[str] | None | |
decision_id | Optional[str] | None | |
incident_id | Optional[str] | None | |
reached | bool | True | |
reason | str | '' |
GatewayStatus
class GatewayStatus(TypedDict)GET /api/v1/gateways/:id/status; the twin of the TypeScript SDK's GatewayStatus.
status, reportedAt, appliedConfigVersion and desiredConfigVersion are always sent. The other keys are None when the gateway is unreachable, and absent when its last heartbeat did not report them.
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
activeWorkspaces | Optional[int] | — | |
appliedConfigVersion | Optional[int] | — | |
desiredConfigVersion | int | — | |
lastError | Optional[str] | — | |
litellmReachable | Optional[bool] | — | |
proxyVersion | Optional[str] | — | |
reportedAt | Optional[str] | — | |
status | Literal['online', 'degraded', 'unreachable', 'pending'] | — | |
uptimeSeconds | Optional[int] | — |
IntuticGateHold
class IntuticGateHold(reason: str, hold_id: Optional[str])Bases: IntuticGateRefusal
Raised when a hold rule stopped the call to ask a person first (code HELD). A subclass, so a caller that stops on every refusal stops on this too; one that tells the user about holds catches it first.
hold_id names the hold in the review queue, and is None when the hold could not be recorded (no control plane to record it in), in which case there is nothing to approve yet. The message starts [Intutic Governance] HELD:, as the hook gates print a hold, and says who can approve it and when a retry passes. See hold.py.
IntuticGateRefusal
class IntuticGateRefusal(reason: str, code: str, incident_id: Optional[str] = None)Bases: ClawdeError
Raised when a tool call must not run.
The python-raise contract: the exception's message begins [Intutic Governance] BLOCKED: — the same family the Open WebUI filter raises — so harnesses and log scrapers that already recognise that prefix recognise this refusal too. The structured fields (reason, code, one of GATE_REFUSAL_CODES, and incident_id) carry the machine-readable version.
ProxyRefusal
class ProxyRefusal(_ProxyRefusalBase)Attributes
| Name | Type | Default | Description |
|---|---|---|---|
rule_id | str | — |
Functions
guard()
def guard(
func: Optional[Callable] = None,
*,
name: Optional[str] = None,
gate: Optional[Gate] = None,
) -> CallableDecorator: evaluate the gate before the tool body runs.
Usage:
@guard
def shell(command: str) -> str: ...
@guard(name="shell") # explicit tool name
def run_command(command: str) -> str: ...On deny the wrapper raises IntuticGateRefusal and the body never runs; the deny has already been reported via hook-events by the gate. gate overrides the process-wide instance registered with install(); without either, calling the wrapped tool raises RuntimeError rather than silently running unguarded.
guard_tools()
def guard_tools(tools: Iterable[Any], *, gate: Optional[Gate] = None) -> listWrap a list of tools (LangChain/LangGraph/smolagents tool objects, or plain callables) so each is gated before execution.
Duck-typed on purpose — no langchain/smolagents import, no hard dependency:
- an object with a callable
.func(StructuredTool,@tool-created Tool) gets its.funcwrapped in place; - otherwise an object with
._run(LangChain BaseTool subclass) gets._runwrapped in place (set via object.setattr, so pydantic models that restrict attribute assignment still work); - otherwise an object with
.forward(smolagentsToolsubclass —Tool.__call__dispatches toself.forward(...)) gets.forwardwrapped in place, the same way. Added when the smolagents adapter (adapters/smolagents.py) was built: without this branch, a smolagentsToolinstance is stillcallable(it has__call__), so it fell into the plain-callable branch below and got REPLACED by a bare wrapper function — silently dropping the.name/.inputs/ schema attributesToolCallingAgentneeds to expose it to the model. The.forwardbranch keeps the originalToolobject (and its schema) intact, only swapping its execution method, matching the._runbranch's contract exactly; - a plain callable is returned wrapped by
@guard— which is also how CrewAI and AutoGen tools are guarded, directly with the decorator.
Already-guarded entries pass through untouched, so calling this twice does not double-gate. Returns the same objects (mutated in place) in order.
intutic_headers()
def intutic_headers(
session_id: Optional[str] = None,
workspace_id: Optional[str] = None,
harness: str = 'langgraph',
) -> Dict[str, str]Default headers for a ChatOpenAI/OpenAI client pointed at the Intutic proxy.
x-session-id matters: the proxy defaults an unset one to the literal "unknown", which merges every run into a single dashboard session. x-intutic-harness attributes traces to this adapter — the proxy honours it for trace attribution (unknown headers are ignored by older proxies, so it is always safe to send).
Usage:
ChatOpenAI(base_url=..., default_headers=intutic_headers(session_id=run_id))stream_refusal()
def stream_refusal(text: str) -> Optional[ProxyRefusal]The refusal named anywhere in a stream's text, or None when there is none. Pass the whole body, or each line as it arrives.
verify_evidence_archive()
def verify_evidence_archive(
archive: Dict[str, Any],
jwks: Optional[SigningJwks],
) -> EvidenceVerificationChecks an evidence archive the way it was sealed: the manifest's hashes, then the signature over the archive hash against the published key its keyId names (jwks, from ControlPlaneClient.get_signing_keys() or a saved copy; None when it could not be had).
verified is True only for an archive whose hashes match and whose signature a published key accepts. An unsigned archive, or one whose key is not published, is not verified even when its hashes match: nothing then shows who produced it.
verify_integrity_root()
def verify_integrity_root(
root: Mapping[str, Any],
jwks: Optional[SigningJwks],
) -> IntegrityRootSignatureStateChecks a sealed root's signature against the published key the root itself names (jwks, from ControlPlaneClient.get_signing_keys() or a saved copy; None when it could not be had). Pass the root of ControlPlaneClient.get_integrity_root().
Only "invalid" says the root was changed: a published key rejected it. "unverifiable" (the key is not published, or the root names an algorithm or preimage version this build cannot check) and "keys_unavailable" are no verdict, and "unsigned" is a deployment that had no signing key. The key is chosen by signing_key_id and never by trying others: a root that verifies under some other key is not the same claim.
Constants
PROXY_REFUSALS
PROXY_REFUSALS: Dict[str, Tuple[int, str]] = ...refusal code -> (HTTP status, verdict). The same table as PROXY_REFUSALS in the TypeScript SDK.
REFUSAL_HEADER
REFUSAL_HEADER = 'x-intutic-refusal'The headers the proxy sets on a 200 that is a refusal in the shape of an answer: the response gate withheld a tool call the model made, output DLP withheld the body, or the cost-prediction gate answered instead of the model. The first names the refusal's code, the second the rule that decided.
REFUSAL_RULE_HEADER
REFUSAL_RULE_HEADER = 'x-intutic-refusal-rule'STREAM_REFUSAL_MARKER
STREAM_REFUSAL_MARKER = ': intutic-refusal 'The prefix of the SSE comment line that names a refusal on a stream: : intutic-refusal {"code", "rule", "message"}. The response headers went out before the refusal happened, so a stream says it in band; every SSE parser skips comment lines, so clients that do not look for it are unaffected.
UPSTREAM_ATTEMPTS_HEADER
UPSTREAM_ATTEMPTS_HEADER = 'x-intutic-upstream-attempts'Set by the proxy when its retry layer made more than one upstream call.
UPSTREAM_FALLBACK_HEADER
UPSTREAM_FALLBACK_HEADER = 'x-intutic-upstream-fallback-from'Set by the proxy when a fallback answered: the model whose retries ran out.