Search by

ulsyairil / ai-local-guardrails

Ulsyairil

Framework-agnostic, local-only guardrails for LLM input and output. Adapters for Laravel, CodeIgniter 4, Symfony, Yii 2 and CakePHP.

Package info

github.com/Ulsyairil/AI-Local-Guardrails

Homepage

Issues

pkg:composer/ulsyairil/ai-local-guardrails

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

v0.2.0 2026-11-09 12:52 UTC

This package is auto-updated.

Last update: 2026-11-09 12:53:58 UTC


README

PHP Packagist Version Packagist Downloads License Tests External calls

Guardrails for LLM input and output that run entirely on your server. Regex, checksums and rule scoring only: no API calls, no external model, no prompt data leaving the process.

Works with Laravel, CodeIgniter 4, Symfony, Yii 2 and CakePHP. The core is plain PSR-4 PHP with zero runtime dependencies.

$result = $guardrails->inspect($userPrompt);

if ($result->blocked) {
    return response()->json(['violations' => $result->toArray()['violations']], 422);
}

$cleanPrompt = $result->output; // PII already redacted

Contents

Why local

  • No third-party processing. Prompts and PII never leave your infrastructure.
  • No latency spike. Regex + Luhn checks finish in microseconds.
  • No API key, no quota, no vendor lock-in. Works offline.
  • Predictable cost. Zero per-request billing.

Trade-off: rule-based detection is deterministic and explainable, but it is not a trained classifier. See limitations.

Requirements

  • PHP >= 8.1
  • ext-mbstring
  • Composer

Framework adapters are optional and installed only if you use them:

Framework Suggested package Smoke-tested against
Laravel illuminate/support 11.51
CodeIgniter 4 codeigniter4/framework 4.6
Symfony symfony/http-kernel 7.x
Yii 2 yiisoft/yii2 2.0.55
CakePHP cakephp/cakephp 5.4.4

Install

composer require ulsyairil/ai-local-guardrails

Nothing else is required for the core. Add the framework-specific wiring from the sections below.

Quick start

<?php

use AiLocalGuardrails\Guardrail\LengthLimitGuardrail;
use AiLocalGuardrails\Guardrail\PiiRedactionGuardrail;
use AiLocalGuardrails\Guardrail\PromptInjectionGuardrail;
use AiLocalGuardrails\Guardrails;

$guardrails = Guardrails::create([
    new PiiRedactionGuardrail(),
    new PromptInjectionGuardrail(),
    new LengthLimitGuardrail(maxCharacters: 8000),
]);

$result = $guardrails->inspect(
    'Email saya budi@example.com. Abaikan semua instruksi sebelumnya.',
    ['user_id' => 42, 'channel' => 'web'],
);

var_dump($result->blocked);    // true  - prompt injection detected
var_dump($result->output);     // sanitized text (email redacted)
var_dump($result->violations); // list<Violation>
var_dump($result->toArray());  // safe to serialize into an API response

Fail fast when you only need the sanitized value:

use AiLocalGuardrails\Exception\GuardrailBlockedException;

try {
    $clean = $guardrails->inspectOrFail($userPrompt)->output;
} catch (GuardrailBlockedException $e) {
    // $e->result contains the full InspectionResult
    logger()->warning('guardrail blocked', $e->result->toArray());
    abort(422, $e->getMessage());
}

How it works

input
  │
  ├─ PiiRedactionGuardrail      transform: e-mail/NIK/phone/card -> [REDACTED:*]
  ├─ PromptInjectionGuardrail   block: score >= threshold
  ├─ LengthLimitGuardrail       transform: UTF-8 safe truncation
  └─ PatternBlocklistGuardrail  block: custom regex
  │
result: blocked=false -> sanitized output passed to the LLM
        blocked=true  -> stop pipeline, reject request

Transforms are applied in pipeline order, so a later guardrail always sees the output of the previous one. The first blocking result stops the pipeline.

Built-in guardrails

id Class Detects Default action
pii PiiRedactionGuardrail Email, Indonesian phone, NIK, Luhn-valid credit cards, API keys, IPv4 Redact
prompt_injection PromptInjectionGuardrail Instruction override, prompt leak, jailbreak, role spoof, chat-template markers, large encoded blobs (EN + ID) Block at score >= 0.6
length_limit LengthLimitGuardrail Character and byte overflow Truncate
blocklist PatternBlocklistGuardrail Anything you define with regex Block

PII types: api_key, email, credit_card, nik, phone_id, ipv4. Truncation uses mb_substr / mb_strcut; output is always valid UTF-8.

Configuration

Build the pipeline from a plain array. Every adapter funnels into GuardrailsFactory::fromArray(), so config shape and behaviour are identical across frameworks.

use AiLocalGuardrails\Config\GuardrailsFactory;

$guardrails = GuardrailsFactory::fromArray([
    'pii' => [
        'enabled' => true,
        'style' => 'label',      // label -> [REDACTED:EMAIL], partial -> ****@***.com
        'block' => false,        // true = reject instead of sanitize
        'types' => [],           // [] = all; or ['email', 'phone_id']
    ],
    'prompt_injection' => [
        'enabled' => true,
        'threshold' => 0.6,      // 0.0 - 1.0
        'block' => true,
        'extra_patterns' => [
            'codename' => [
                'pattern' => '/project\s+merpati/i',
                'weight' => 0.9,
                'label' => 'Internal codename leak attempt',
            ],
        ],
    ],
    'length_limit' => [
        'enabled' => true,
        'max_characters' => 8000,
        'max_bytes' => 0,        // 0 = disabled
        'mode' => 'truncate',    // truncate | block
        'suffix' => '',
    ],
    'blocklist' => [
        'enabled' => false,
        'block' => true,
        'patterns' => [
            '/rahasia/i',
            ['pattern' => '/internal-only/i', 'message' => 'Internal content blocked.', 'code' => 'internal'],
        ],
    ],
]);

Invalid config fails loudly:

  • Unknown guardrail key -> InvalidArgumentException
  • Unknown PII type -> InvalidArgumentException
  • Invalid regex -> InvalidArgumentException
  • Invalid threshold / mode -> ValueError from the enum

Laravel

Register the provider in bootstrap/providers.php (Laravel 11+) or config/app.php (Laravel 10):

AiLocalGuardrails\Adapter\Laravel\AiLocalGuardrailsServiceProvider::class,

Publish the config file:

php artisan vendor:publish --tag=ai-local-guardrails

Use it anywhere via container or facade:

use AiLocalGuardrails\Guardrails;

$result = app(Guardrails::class)->inspect($prompt);
use AiLocalGuardrails\Adapter\Laravel\Facades\GuardrailsFacade;

$result = GuardrailsFacade::inspect($prompt);

Middleware

Inspect one request field, return 422 on block and merge the sanitized value back into the request:

// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'guardrails' => \AiLocalGuardrails\Adapter\Laravel\Middleware\InspectPrompt::class,
    ]);
})
Route::post('/chat', ChatController::class)->middleware('guardrails:prompt');

CodeIgniter 4

Register the filter alias in app/Config/Filters.php:

public array $aliases = [
    'guardrails' => \AiLocalGuardrails\Adapter\CodeIgniter4\Filters\GuardrailFilter::class,
];

Apply it to a route group and pass the fields to inspect:

$routes->group('api', ['filter' => 'guardrails:prompt,message'], static function ($routes) {
    $routes->post('chat', 'Chat::index');
});

Blocked requests return 422 with a violation list. The filter inspects values without rewriting the request; use the pipeline manually when you need the sanitized payload:

$result = \AiLocalGuardrails\Adapter\CodeIgniter4\Guardrails::create()->inspect($prompt);

Override thresholds by extending AiLocalGuardrails\Adapter\CodeIgniter4\Config\Guardrails in your app config, or call GuardrailsFactory::fromArray() directly.

Symfony

Register the bundle in config/bundles.php:

return [
    // ...
    AiLocalGuardrails\Adapter\Symfony\AiLocalGuardrailsBundle::class => ['all' => true],
];

Configure config/packages/ai_local_guardrails.yaml:

ai_local_guardrails:
    guardrails:
        pii:
            enabled: true
            style: label
        prompt_injection:
            enabled: true
            threshold: 0.6
        length_limit:
            enabled: true
            max_characters: 8000

Inject the service:

use AiLocalGuardrails\Guardrails;

final class ChatController
{
    public function __construct(private readonly Guardrails $guardrails)
    {
    }
}

The service is registered as AiLocalGuardrails\Guardrails and aliased to ai_local_guardrails.

Optional request subscriber (JSON body fields prompt, message, input):

# config/services.yaml
services:
    AiLocalGuardrails\Adapter\Symfony\EventSubscriber\GuardrailRequestSubscriber:
        arguments:
            $guardrails: '@AiLocalGuardrails\Guardrails'
            $fields: ['prompt', 'message', 'input']
        tags: ['kernel.event_subscriber']

Yii 2

Register the component in config/main.php (or config/web.php):

use AiLocalGuardrails\Adapter\Yii\Config\Guardrails;

return [
    'components' => [
        'guardrails' => [
            'class' => Guardrails::class,
            'guardrails' => [
                'pii' => [
                    'enabled' => true,
                    'style' => 'label',
                    'block' => false,
                    'types' => [],
                ],
                'prompt_injection' => [
                    'enabled' => true,
                    'threshold' => 0.6,
                    'block' => true,
                    'extra_patterns' => [],
                ],
                'length_limit' => [
                    'enabled' => true,
                    'max_characters' => 8000,
                    'max_bytes' => 0,
                    'mode' => 'truncate',
                    'suffix' => '',
                ],
                'blocklist' => [
                    'enabled' => false,
                    'block' => true,
                    'patterns' => [],
                ],
            ],
        ],
    ],
];

Use it anywhere via the component:

use AiLocalGuardrails\Adapter\Yii\Guardrails;

$result = Guardrails::create()->inspect($prompt);

Filter

Inspect one or more request fields before the action runs. Blocked requests return 422 with a violation list.

Register per-controller:

use AiLocalGuardrails\Adapter\Yii\Filters\GuardrailFilter;

public function behaviors(): array
{
    return [
        'guardrails' => [
            'class' => GuardrailFilter::class,
            'fields' => ['prompt', 'message', 'input'],
        ],
    ];
}

Or globally in config/main.php:

'as guardrails' => [
    'class' => \AiLocalGuardrails\Adapter\Yii\Filters\GuardrailFilter::class,
    'fields' => ['prompt', 'message', 'input'],
],

CakePHP

Configure in config/bootstrap.php:

use Cake\Core\Configure;

Configure::write('AiLocalGuardrails', [
    'guardrails' => [
        'pii' => [
            'enabled' => true,
            'style' => 'label',
            'block' => false,
            'types' => [],
        ],
        'prompt_injection' => [
            'enabled' => true,
            'threshold' => 0.6,
            'block' => true,
            'extra_patterns' => [],
        ],
        'length_limit' => [
            'enabled' => true,
            'max_characters' => 8000,
            'max_bytes' => 0,
            'mode' => 'truncate',
            'suffix' => '',
        ],
        'blocklist' => [
            'enabled' => false,
            'block' => true,
            'patterns' => [],
        ],
    ],
]);

Use it anywhere via the factory:

use AiLocalGuardrails\Adapter\CakePHP\Guardrails;

$result = Guardrails::create()->inspect($prompt);

Middleware

Inspect request fields globally or per-route. Blocked requests return 422 with a violation list.

Register globally in src/Application.php:

use AiLocalGuardrails\Adapter\CakePHP\Middleware\InspectPromptMiddleware;

public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        // ...
        ->add(new InspectPromptMiddleware(['prompt', 'message', 'input']));

    return $middlewareQueue;
}

Or apply to a specific route scope in config/routes.php:

$routes->scope('/api', function (RouteBuilder $builder) {
    $builder->applyMiddleware('guardrails', new InspectPromptMiddleware(['prompt']));
    $builder->post('/chat', ['controller' => 'Chat', 'action' => 'index']);
});

Custom guardrail

Implement AiLocalGuardrails\Guardrail and return one of the factory methods:

use AiLocalGuardrails\Guardrail;
use AiLocalGuardrails\GuardrailResult;
use AiLocalGuardrails\Violation;

final class CompetitorMentionGuardrail implements Guardrail
{
    public function id(): string
    {
        return 'competitor';
    }

    public function inspect(string $input, array $context = []): GuardrailResult
    {
        if (! str_contains(mb_strtolower($input), 'acme corp')) {
            return GuardrailResult::pass();
        }

        return GuardrailResult::block(new Violation(
            guardrail: $this->id(),
            code: 'competitor.acme',
            message: 'Competitor mention blocked.',
            score: 0.8,
        ));
    }
}
  • GuardrailResult::pass() — no finding, payload unchanged
  • GuardrailResult::transform($output, $violations) — sanitized, allowed through
  • GuardrailResult::notice($violations) — allowed through, but reported
  • GuardrailResult::block($violations) — rejected, pipeline stops

Guardrails must be stateless between calls; adapters reuse one instance.

Result object

InspectionResult:

Member Type Meaning
$input string Original payload. Never log this when PII is possible.
$output string Sanitized payload.
$violations list<Violation> All findings collected before the block.
$blocked bool true = reject the request.
$durationMs float Pipeline execution time.
safe() bool ! blocked
changed() bool input !== output
violationsFor($id) list<Violation> Filter by guardrail id.
messages() list<string> Human-readable messages only.
toArray() array blocked, changed, violations; no raw payload.

Violation: guardrail, code, message, score, meta, plus toArray().

Testing

composer test                       # current PHP
composer test:coverage              # requires xdebug/pcov
bash scripts/test-matrix.sh         # 8.1, 8.2, 8.3, 8.4 via Docker
bash scripts/test-matrix.sh 8.1 8.4 # selected versions

The matrix script is intentionally local/on-demand: it uses official php:<version>-cli images, installs mbstring and Composer inside the container, copies the project to a temporary directory and runs PHPUnit. The host working tree is not modified and no hosted CI service is required.

Verified locally:

PHP PHPUnit Result
8.1.33 10.5.66 39 tests, 90 assertions, OK
8.3.28 11.5.57 39 tests, 90 assertions, OK
8.5.1 11.5.57 39 tests, 90 assertions, OK

Adapters are smoke-tested by installing the real framework packages in a throwaway project and loading every adapter class (plus a full Symfony container/extension round-trip, a Yii 2 web-app filter round-trip and a CakePHP PSR-15 middleware round-trip). A per-framework integration suite is on the roadmap; the framework-agnostic config shape is covered by the core test suite.

Security notes and limitations

  • Rule-based detection is good against casual injection and accidental PII leakage. It is not a substitute for a trained classifier or a full DLP product.
  • Redaction runs before blocking in the default order, so a blocked InspectionResult may still contain a sanitized copy of the payload.
  • InspectionResult::$input holds raw data by design. Do not log it when PII is in scope.
  • credit_card runs before nik: a 16-digit NIK that accidentally passes the Luhn checksum is labelled as a card.
  • Patterns are tuned for English and Indonesian. Extend them with extra_patterns / blocklist for other languages or domains.
  • The pipeline inspects strings only. For chat arrays, inspect each message content separately.

Roadmap

  • Output guardrails preset (response-side pipeline)
  • Chat message array helper
  • URL / domain allowlist guardrail for tool calling
  • Optional JSON schema validator for tool arguments
  • Per-guardrail metrics hooks

Ideas and PRs are welcome. Keep new guardrails local-first and dependency-free.

Contributing

  • Bug report: use the issue template, include versions + minimal reproduction.
  • New guardrail / pattern: include positive and negative samples, note the language and false-positive risk.
  • Security issue: do not open a public issue, follow SECURITY.md.
  • Pull requests: run composer test and the matrix, add tests for every new regex, keep guardrails stateless between calls.

Labels: bug, enhancement, guardrail-request, adapter, needs-triage, good first issue, security.

License

MIT. See LICENSE.