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.