Skip to content

Independent PHP Consumer

Use published axisvela/praxo-core 0.7.0 in your own application with a patched PHP release and Composer 2. On Windows use PHP 8.2.35-1, 8.3.36-1, 8.4.27-1, 8.5.12-1, or newer patched builds, which include Brotli 1.2.1 and the fix for CVE-2026-111497. No PRAXO-CORE source checkout, API key, model service, or AxisVela product is required. Installation needs Packagist access or a cached Composer archive.

This is an extended-language starter. It demonstrates the allow/deny path but is not one of the four pilot languages that carry the governed negative matrix.

Choose the Boundary

This is a fit for an embedded 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.

Create the Application

Declare the exact dependency in composer.json:

{
  "require": {
    "axisvela/praxo-core": "0.7.0"
  }
}

Then run composer install. Do not use a path repository, a local archive, or an autoload.php include from a source checkout for acceptance.

Application Source

Use the following as starter.php, rendered from the tested source.

<?php

declare(strict_types=1);

$autoload = __DIR__ . '/vendor/autoload.php';
if (is_file($autoload)) {
    require $autoload;
} else {
    require __DIR__ . '/../../language/php/src/autoload.php';
}

use Praxo\Core\Action;
use Praxo\Core\Agent;
use Praxo\Core\AgentOptions;
use Praxo\Core\Decision;
use Praxo\Core\FuncTool;
use Praxo\Core\Message;
use Praxo\Core\Model;
use Praxo\Core\PolicyHook;
use Praxo\Core\PraxoException;
use Praxo\Core\Request;
use Praxo\Core\Response;
use Praxo\Core\Roles;
use Praxo\Core\Session;
use Praxo\Core\ToolCall;
use Praxo\Core\ToolRegistry;

final class FixtureModel implements Model
{
    public function __construct(private readonly string $target)
    {
    }

    public function generate(Request $request): Response
    {
        $last = $request->messages[count($request->messages) - 1];
        if ($last->role === Roles::TOOL) {
            return new Response(Message::assistant('recorded'));
        }
        return new Response(Message::assistant('', [
            new ToolCall('record-1', 'record', ['target' => $this->target]),
        ]));
    }
}

final class StarterPolicy implements PolicyHook
{
    public function authorize(Action $action): Decision
    {
        return match ($action->kind) {
            'model.generate' => Decision::allow('model generation'),
            'tool.invoke' => $action->toolName === 'record'
                && ($action->arguments['target'] ?? null) === 'public'
                ? Decision::allow('public target')
                : Decision::deny('only the public target is permitted'),
            default => Decision::deny('unsupported action'),
        };
    }
}

final class Starter
{
    /** @return array{0: string, 1: int, 2: ?Throwable} */
    public static function runScenario(string $target): array
    {
        $calls = 0;
        $registry = new ToolRegistry();
        $registry->register(new FuncTool('record', 'Record a target in an in-memory counter.',
            static function (array $arguments) use (&$calls): array {
                $calls++;
                return ['output' => 'recorded'];
            }));

        $options = new AgentOptions(new FixtureModel($target));
        $options->tools = $registry;
        $options->allowedTools = ['record'];
        $options->policy = new StarterPolicy();

        try {
            $result = (new Agent($options))->run(new Session('consumer-' . $target), 'Record the requested target.');
            return [$result->output, $calls, null];
        } catch (PraxoException $error) {
            return ['', $calls, $error];
        }
    }

    public static function run(): void
    {
        [$output, $calls, $error] = self::runScenario('public');
        if ($error !== null || $output !== 'recorded' || $calls !== 1) {
            throw new RuntimeException("allow verification failed: output={$output} effects={$calls} error={$error?->getMessage()}");
        }
        echo "allow: recorded; effects=1\n";

        [, $calls, $error] = self::runScenario('private');
        if ($error === null || $error->praxoCode !== 'AGENT_POLICY_DENIED' || $calls !== 0) {
            throw new RuntimeException("deny verification failed: effects={$calls} error={$error?->getMessage()}");
        }
        echo "deny: policy rejected; effects=0\n";
    }
}

if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
    Starter::run();
}

Acceptance Tests

Use the following as test_starter.php, rendered from the tested source.

<?php

declare(strict_types=1);

require __DIR__ . '/starter.php';

[$output, $calls, $error] = Starter::runScenario('public');
if ($error !== null || $output !== 'recorded' || $calls !== 1) {
    throw new RuntimeException("allow: output={$output} effects={$calls} error={$error?->getMessage()}");
}

[, $calls, $error] = Starter::runScenario('private');
if ($error === null || $error->praxoCode !== 'AGENT_POLICY_DENIED' || $calls !== 0) {
    throw new RuntimeException("deny: effects={$calls} error={$error?->getMessage()}");
}

echo "php consumer tests passed\n";

Verify

composer install
composer test

Expected program output:

allow: recorded; effects=1
deny: policy rejected; effects=0

The model is a deterministic fixture, not an LLM. allowedTools limits the tool name; Policy also checks the target argument. A denied private target raises PraxoException with code AGENT_POLICY_DENIED and must not increment the in-memory counter. Keep these tests when replacing the fixture with a real adapter.

For a source checkout without Composer, the starter falls back to language/php/src/autoload.php; that path is not independent consumer evidence.

Runtime Security

PRAXO-CORE does not bundle PHP or Brotli. Use a patched PHP runtime. The PHP Windows builds 8.2.35-1, 8.3.36-1, 8.4.27-1, 8.5.12-1, and newer patched builds include Brotli 1.2.1 and fix the out-of-bounds write in Brotli decompression tracked as CVE-2026-111497. Linux distributions and containers may provide equivalent backports; verify their advisory status before deployment.

Scope

This starter establishes only the stated package and fixture behavior. It does not add the governed negative matrix, prove authentic identity, provide durable approvals, reconcile effects, or establish isolation. Continue with the PHP implementation guide and the security model.