Independent Python Consumer¶
Use published praxo-core==0.8.1 in your own application with Python 3.11 or
newer. No PRAXO-CORE source checkout, API key, model service, or AxisVela product
is required. Installation needs PyPI access or a cached wheel.
Choose the Boundary¶
This is a fit for an embedded async model/tool loop with explicit tool scope and policy. Your application still owns model access, authentic identity, persistent approvals, external effects, and isolation. It is not a hosted agent platform, durable graph engine, or sandbox service. See when not to use Core.
Install in a New Application¶
In a new application directory, create a virtual environment. Use its Python directly; activation is not necessary.
POSIX:
python3 -m venv .venv
.venv/bin/python -m pip install --index-url https://pypi.org/simple praxo-core==0.8.1
PowerShell:
python -m venv .venv
.venv\Scripts\python.exe -m pip install --index-url https://pypi.org/simple praxo-core==0.8.1
Do not use pip install -e, a repository path, or PYTHONPATH to substitute the
SDK. The acceptance tests check the installed package version, its location in
the virtual environment, and the absence of a direct-path installation record.
Application Source¶
Use the following as starter.py. The documentation site renders the
tested application source
directly; GitHub readers can follow the source link.
import asyncio
import sys
from typing import TextIO
from praxo.agent import Agent, AgentOptions, Session
from praxo.errors import PolicyDeniedError
from praxo.model import Message, Request, Response, ToolCall
from praxo.policy import Action, Decision, FuncHook
from praxo.tool import Registry
class FixtureModel:
def __init__(self, target: str):
self.target = target
async def generate(self, request: Request) -> Response:
if request.messages[-1].role == "tool":
return Response(message=Message(role="assistant", content="recorded"))
return Response(message=Message(
role="assistant",
tool_calls=[ToolCall(id="record-1", name="record", arguments={"target": self.target})],
))
class RecordTool:
name = "record"
description = "Record a target in an in-memory counter."
risk = "write"
parameters = {
"type": "object",
"required": ["target"],
"properties": {"target": {"type": "string"}},
}
def __init__(self):
self.calls = 0
async def invoke(self, args):
self.calls += 1
return "recorded"
def build_agent(target: str) -> tuple[Agent, RecordTool]:
impl = RecordTool()
tools = Registry()
tools.register(impl)
def authorize(action: Action) -> Decision:
if action.kind == "model.generate":
return Decision(allowed=True)
if action.kind == "tool.invoke":
return Decision(
allowed=action.tool_name == "record" and action.arguments.get("target") == "public",
reason="only the public target is permitted",
)
return Decision(allowed=False, reason="unsupported action")
return Agent(AgentOptions(
model=FixtureModel(target), tools=tools, max_steps=3,
allowed_tools=["record"], policy=FuncHook(authorize),
)), impl
async def run(output: TextIO) -> None:
agent, impl = build_agent("public")
result = await agent.run(Session(id="starter-public"), "Record the requested target.")
if result.output != "recorded" or impl.calls != 1:
raise RuntimeError(f"allow verification failed: output={result.output!r} effects={impl.calls}")
print("allow: recorded; effects=1", file=output)
agent, impl = build_agent("private")
try:
await agent.run(Session(id="starter-private"), "Record the requested target.")
except PolicyDeniedError:
if impl.calls != 0:
raise RuntimeError(f"denied call produced effects={impl.calls}")
else:
raise RuntimeError("private target was not denied")
print("deny: policy rejected; effects=0", file=output)
if __name__ == "__main__":
asyncio.run(run(sys.stdout))
Acceptance Tests¶
Use the following as test_starter.py, rendered from the
tested source.
import importlib.metadata
import io
from pathlib import Path
import sys
import unittest
import praxo
from praxo.agent import Session
from praxo.errors import PolicyDeniedError
from starter import build_agent, run
class StarterTests(unittest.IsolatedAsyncioTestCase):
async def test_allowed_tool(self):
agent, impl = build_agent("public")
result = await agent.run(Session(id="test-public"), "Record the requested target.")
self.assertEqual(result.output, "recorded")
self.assertEqual(impl.calls, 1)
async def test_denied_tool_has_no_effect(self):
agent, impl = build_agent("private")
with self.assertRaises(PolicyDeniedError):
await agent.run(Session(id="test-private"), "Record the requested target.")
self.assertEqual(impl.calls, 0)
async def test_starter_output(self):
output = io.StringIO()
await run(output)
self.assertEqual(output.getvalue(), "allow: recorded; effects=1\ndeny: policy rejected; effects=0\n")
def test_published_dependency(self):
self.assertNotEqual(sys.prefix, sys.base_prefix, "use a dedicated virtual environment")
distribution = importlib.metadata.distribution("praxo-core")
self.assertEqual(distribution.version, "0.8.1")
self.assertEqual(praxo.__version__, "0.8.1")
self.assertTrue(Path(praxo.__file__).resolve().is_relative_to(Path(sys.prefix).resolve()))
self.assertIsNone(distribution.read_text("direct_url.json"), "install from the registry, not a local path")
if __name__ == "__main__":
unittest.main()
Verify¶
POSIX:
.venv/bin/python -m pip check
.venv/bin/python -I -m unittest discover -s . -v
.venv/bin/python -I starter.py
PowerShell:
.venv\Scripts\python.exe -m pip check
.venv\Scripts\python.exe -I -m unittest discover -s . -v
.venv\Scripts\python.exe -I starter.py
Expected program output:
allow: recorded; effects=1
deny: policy rejected; effects=0
The model is a deterministic fixture, not an LLM. allowed_tools limits the tool
name; Policy also checks the target argument. Empty scope does not mean deny-all
in the current API. A denied private target must never reach the in-memory effect
counter. Keep these tests when replacing the fake model with a real adapter.
Governed Acceptance¶
test_governed.py adds five negative checks:
.venv/bin/python -I -m unittest discover -s . -p "test_governed.py" -v
| Check | Required outcome |
|---|---|
| Missing mandatory policy | application guard rejects before a run; zero tool effects |
| Empty tool scope | application guard rejects; zero tool effects |
| Middleware changes final arguments | final operation is reauthorized; denial prevents the effect |
| Approval edit changes arguments | edited operation is reauthorized; denial prevents the effect |
| Policy service error | run fails closed; zero tool effects |
Use the tested source. The guard is application-level: Core keeps its ordinary nil-policy behavior. These checks do not prove authentic identity, durable approvals, effect reconciliation, or a general security profile.
These checks establish only the stated package and fixture behavior, not live model quality, production security, or durable execution guarantees. Continue with the Python API and implementation guide and the security model.