Skip to content

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.