This release is a pre-release and may not be stable for production use.
Felspar for Python (alpha)
Felspar gives agents a shared, versioned record of what your company has approved. This package is the Python client and a synchronous action gate: before an agent acts, the gate checks the action against a verified Felspar decision and denies anything it cannot verify.
This is an alpha. The API may change between releases, and it does not yet match
a stable @felspar/sdk 1.0 contract; it implements the tested 0.4.1 gate
behavior. Pin the exact version you test.
Install
pip install felspar --pre
Python 3.11+ is required. The typed client for all six JSON operations is included
under felspar.generated.
Quickstart
You need an agent key and a job in a released Pack. Set FELSPAR_ORIGIN to
https://api.felspar.ai, and keep the key in the environment, never in code.
import os
from felspar import Client, ActionGate, denial_text
from felspar.generated.api.agents import decide_job
from felspar.generated.models.decision_input_any_of_variant_0 import DecisionInputAnyOfVariant0
# request_json is the job's actual documented request, including source evidence.
# session_id and tool rules are controlled by your application, never the model.
with Client(os.environ["FELSPAR_ORIGIN"], os.environ["FELSPAR_AGENT_KEY"]) as client:
gate = ActionGate(
rules={"job": "review-request", "tools": {"reply": {
"writes": "request-reply", "fields": ["caseId", "text"],
"entity": {"field": "caseId"}, "outputCheck": {"fields": ["text"]},
}}},
verify=client.verify, describe=client.describe,
verify_output_check=client.verify_output_check,
)
body = DecisionInputAnyOfVariant0.from_dict(request_json)
response = decide_job.sync_detailed("review-request", client=client.transport, body=body)
if response.status_code != 200:
raise RuntimeError("Decision request failed")
gate.decisions.record(session_id, {"job": "review-request", "entity": body.entity_id}, response.parsed)
# Generate text, call generated check_output with OutputCheckInput, then:
# gate.decisions.record_output_check(session_id, "review-request", checked.parsed)
verdict = gate.check(session_id, "reply", {"caseId": case_id, "text": text})
if not verdict["allow"]:
raise RuntimeError(denial_text(verdict))
reply(case_id, text)
The snippet's request, case, text and reply function belong to your application; job/output IDs must match its released Pack.
The gate
ActionGate.check(session_id, tool_name, input) returns a JSON verdict with
allow, reason, and either the verified decision or optional escalate and
escalation. denial_text formats the same denial as TypeScript. Rules retain
wire-compatible camelCase keys so shared configuration and fixtures are exact.
Use exactly one of outcomes or writes, and an explicit top-level field allow
list. refuse takes precedence. Tool names match exactly.
Case binding supports provided/exact and ASCII case-insensitive IDs, issued,
fields and content identities, explicit entity fields, or one implicit case.
Legacy company-domain jobs bind all recipients; shared providers/public suffixes
are excluded by the same compact list as TypeScript (sharedDomains extends it).
Test recipients must all be listed. The newest recorded decision for that case
wins. Replay must match job, case ID/mode and any releases pin. Evidence expires
at the server-reported boundary; no freshness limit means no additional local
expiry, as in TypeScript. Missing evidence produces no usable decision on the
server. writes reads outputs from the release; output checks require exact
UTF-8 text, decision digest, output identity and a fresh server readback.
Record only direct Felspar responses, never a model's reconstruction. The recorder
copies data and bounds sessions (1000) and records per session (200), and supports
forget(session_id). It is process-local; serialize access to a session and do
not share it across tenants. Hooks must use host-owned session IDs. Verification
is an action-time read, not a one-use authorization or a guarantee of downstream
execution. Customers own tool execution, credentials and idempotency.
The generated brief route follows the agent grant, so a changed grant denies an older decision rather than substituting another release's description. The Python facade does not yet provide the TypeScript retained-release description route. JSON responses are supported; MCP/content-part unwrapping and split request/result recording are intentionally outside this JSON-only API. Network and malformed transport errors are sanitized and deny; no redirects or ambient HTTP proxies are used. Production requires HTTPS; loopback HTTP supports local testing.
OpenAI Agents hook
This hook uses the SDK's function-tool input guardrail API, tested with
openai-agents==0.24.0 (upstream contract).
Attach it to each action tool. The gate's tool rule must use the function's exact
name, and the host's Runner context must contain session_id; never take it from
tool input.
import asyncio
import json
from agents import Agent, Runner, ToolGuardrailFunctionOutput, function_tool, tool_input_guardrail
from felspar import denial_text
def guardrail(gate):
@tool_input_guardrail
async def verify_before_action(data):
try:
context = data.context
arguments = json.loads(context.tool_arguments)
session = context.context["session_id"]
if not isinstance(session, str) or not session:
raise ValueError("missing session")
verdict = await asyncio.to_thread(gate.check, session, context.tool_name, arguments)
if verdict["allow"] is True:
return ToolGuardrailFunctionOutput.allow()
if verdict["allow"] is not False:
raise ValueError("invalid verdict")
return ToolGuardrailFunctionOutput.reject_content(denial_text(verdict))
except Exception:
return ToolGuardrailFunctionOutput.reject_content(
"Action denied: unreadable action, verdict or missing host session."
)
return verify_before_action
@function_tool(tool_input_guardrails=[guardrail(gate)])
def reply(caseId: str, text: str) -> str:
"""Write the verified reply for this case."""
return write_reply(caseId, text)
agent = Agent(name="request assistant", tools=[reply])
# Supply a host-owned session that already contains the direct decision/check.
result = await Runner.run(agent, prompt, context={"session_id": session_id})
This covers function tools attached to this guardrail, not hosted tools or other unguarded execution paths. A denied call returns the denial to the model without running the function.
Documentation
Metadata
Release files for felspar 0.1.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| felspar-0.1.0a1.tar.gz | 122.2 kB | Without provenance | Details for felspar-0.1.0a1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| felspar-0.1.0a1-py3-none-any.whl | Python 3 | none | any | Details for felspar-0.1.0a1-py3-none-any.whl |
Total release size: 468.7 kB
Release files / felspar-0.1.0a1.tar.gz
| Download URL | felspar-0.1.0a1.tar.gz |
|---|---|
| Size | 122.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3540b3f76d78a9c560144925e0babe626705920d66bd3942f043be4102429909
|
|
BLAKE2b-256 checksum How to use checksums |
aba74d72cebe697bd6cdb4783a89cc202364f6c94bdac08481552e92a37f8a6b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / felspar-0.1.0a1-py3-none-any.whl
| Download URL | felspar-0.1.0a1-py3-none-any.whl |
|---|---|
| Size | 346.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f0247c2a0d63a36420efe1f4646ba08e51857fb2f1d689313fc11280bf853440
|
|
BLAKE2b-256 checksum How to use checksums |
14a8ee84fd957fd7e117266fb70ca93a59b01fb563119b26bd207bc27b824e90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for felspar 0.1.0a1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.