tork / governance
On-device AI governance SDK - PII detection, redaction, and cryptographic receipts
Requires
- php: ^8.1
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-05 06:15:45 UTC
README
On-device AI governance for PHP applications. PII detection, redaction, and cryptographic compliance receipts.
Installation
composer require tork/governance
Quick Start
<?php use Tork\Governance\Core\Tork; $tork = new Tork(); $result = $tork->govern("Contact john@example.com or call 555-123-4567"); echo $result->action; // "redact" echo $result->output; // "Contact [EMAIL_REDACTED] or call [PHONE_REDACTED]"
Upgrading to 1.0.0
Tork::govern() now detects PII through the same 10-type Tier 1 table
Tork::scanToolResult() already used (Pii::PII_PATTERNS), instead of its
own separate 5-pattern set — the two no longer drift into different results
for the same input. piiTypesDetected type keys are now lowercase
snake_case (ssn, not SSN), the credit_card and ip_address redaction
labels changed to [CARD_REDACTED] and [IP_REDACTED] (the other three —
ssn, email, phone — kept their existing labels), five more types are
now detected (address, date_of_birth, passport, drivers_license,
bank_account), and a customPatterns match alone no longer drives
$result->action to redact/deny/escalate on its own (it still redacts
$output text). See CHANGELOG.md for the
full old-label/key → new-label/key mapping.
Country PII detection
24 country profiles, 54 patterns and 20 check digits, generated from Tork's own
country registry (bundle 1.2.0) and computed entirely on-device.
Countries: AU, US, GB, EU, AE, SA, NG, IN, JP, CN, KR, BR, CA, ZA, GH, IT, KE, MU, MX, MY, PK, SG, TH, ID.
A country's patterns switch on when the text activates that country — the same content signals the cloud uses — so ordinary business text is not measured against 54 national-identifier patterns it could never contain. On the 1,159-line business corpus this SDK is tested against, nothing is redacted.
use Tork\Governance\Core\Pii; $r = Pii::detect('South African ID number 8001015009087 for the FICA check.'); $r['regions']; // ['ZA'] $r['countryLabels']; // ['ZA_ID'] $r['redactedText']; // 'South African ID number [ZA_ID_REDACTED] for the FICA check.'
Pass $region to force profiles on when you already know the jurisdiction:
use Tork\Governance\Core\Tork; $tork = new Tork(); $result = $tork->govern('Documento 529.982.247-25 arquivado.', region: ['br']); // $result->output === 'Documento [CPF_REDACTED] arquivado.'
Three gates keep the false-positive rate down, and all three must pass:
- Activation — one of the country's content signals fires.
- Keyword — for 18 of the 24 national, tax and health identifiers, one of the identifier's keywords must appear within 60 characters before the match or 40 after.
- Check digit — for the 10 identifiers whose issuing authority publishes
the algorithm, a number of the right shape that fails its check digit is not
that country's identifier. Where the algorithm is community-sourced rather
than authority-published (
ca_sin,emirates_id,de_tax_id,kr_rrn,sa_national_id) the checksum is advisory and never rejects a match.
Still cloud-only, and not in this SDK: the near-miss fallback, the slot, context, gravity and name layers, industry profiles, and org configuration.
Agent telemetry
govern() accepts an optional $sessionContext with four optional fields:
agent_id, agent_role, session_id (strings) and session_turn (int).
Fields you set are passed through to the result; fields you leave out (or set
to null) are omitted, and so is session_context when none are set. Unknown
keys are dropped; a wrongly typed value throws InvalidArgumentException.
$result = $tork->govern($text, sessionContext: [ 'agent_id' => 'agent-7', 'agent_role' => 'planner', 'session_id' => 'sess-1', 'session_turn' => 3, ]); $result->toArray()['session_context']; // ['agent_id' => 'agent-7', ...]
Laravel Integration
The SDK auto-registers via Laravel's package discovery.
// In routes/api.php Route::middleware('tork')->group(function () { Route::post('/users', [UserController::class, 'store']); }); // Publish config (optional) php artisan vendor:publish --tag=tork-config
Access in controllers:
$tork = $request->attributes->get('tork'); $receipts = $request->attributes->get('torkReceipts');
Symfony Integration
1. Register the bundle
// config/bundles.php return [ // ... Tork\Governance\Symfony\TorkBundle::class => ['all' => true], ];
2. Configure (optional)
# config/packages/tork.yaml tork: default_action: redact policy_version: '1.0.0' middleware: governInput: true governOutput: true governBody: true
3. Use in controllers
class ApiController extends AbstractController { public function index(Request $request): JsonResponse { $tork = $request->attributes->get('tork'); $receipts = $request->attributes->get('torkReceipts'); // Your logic here... } }
Scanning tool results
A tool result returned by an MCP server — or any external system you do not control — is untrusted input that is about to be appended to a model's context. Tork::scanToolResult() scans it first, on-device, for PII and prompt injection:
use Tork\Governance\Core\Tork; $tork = new Tork(); $report = $tork->scanToolResult( [ 'toolName' => 'lookup_customer', 'serverUri' => 'mcp://crm.internal/customers', 'payload' => $toolResult, // whatever the server returned (array/stdClass/scalar) ], ['blockOnInjection' => true] ); if ($report->blocked) { logWarning($report->reason); // do not append anything } else { appendToContext($report->sanitized); // PII masked in place } $report->findings; // [ToolResultFinding{kind: 'pii', type: 'email', count: 1, location: '$.content[0].text'}, // ToolResultFinding{kind: 'injection', type: 'heuristic:instruction_override', count: 1, location: '$.content[0].text'}]
There is also a standalone Tork\Governance\Core\ToolResultScan::scan($input, $options) with the same input/options shape that returns a ToolResultScanResult (sanitized, findings, blocked, reason) and produces no receipt.
- PII uses the same on-device detector (
Pii::detect()) asTork::govern()throughout this SDK — the Tier 1 basic vocabulary below, JS-identical type labels and redaction placeholders, since 1.0.0 (see Upgrading to 1.0.0 if migrating from an earlier version). Matches are masked in place; the payload structure is otherwise unchanged, and a clean payload comes back untouched. - Injection detection is heuristic. A conservative pattern set (
tork-injection-heuristics-v1) covering instruction-override phrases, role reassignment, and exfiltration URLs — ported line-for-line from the JS SDK's regex sources. Every injection finding is typedheuristic:<name>because that is exactly what it is: a regex match over untrusted text, with false positives and false negatives, not a verified determination. WithoutblockOnInjection, matches are reported and the result is still returned; with it,sanitizedisnullso no masked copy can be appended by accident. - Zero network calls. The scan is pure and synchronous PHP — no curl handle, no stream-wrapper I/O, no socket is ever opened on this path.
- Recorded on the receipt as counts only.
$report->receipt->toolResultScancarriesattested_by: 'client',capture_mode: 'edge', the tool name and server URI, counts by kind and type, the blocked flag, and the SDK version — snake_case keys in alphabetical order, optional keys (reason,server_uri) omitted rather than nulled, byte-identical in shape to the JS and Python SDKs'tool_result_scanblock. It never carries the payload, a matched value, or a location path. - The receipt's
actionfollows a four-way mapping:blocked→deny; injection findings present (checked first, even alongside PII) →escalate; PII findings only →redact; otherwise →allow. - PHP-specific traversal rule. PHP's
arraytype serves as both JSON array and JSON object, which JS/Python's list-vs-dict types don't need to resolve. The rule applied: an array is walked as a JSON array whenarray_is_list()is true (including an empty array[], matching PHP's ownjson_encode([])default), and as a map/object otherwise; non-identifier map keys (e.g. numeric-string keys) get bracket notation inlocation, e.g.$["5"].stdClassinstances are always walked as objects. SeeToolResultScan's class docblock for the full rule and its cycle-guard/identity implications.
This is a client-side, client-attested control. The scan runs in your process, and the receipt says so: Tork did not execute it and cannot verify it ran at all. Gateway-side enforcement, where a caller cannot skip the scan, is a separate and later control. Do not read a tool_result_scan block as proof that every tool result reaching a model was scanned; read it as a record of the scans a caller chose to run and report.
Parity tier: this port implements Tier 1 — the 10-type basic vocabulary (ssn, credit_card, email, phone, address, ip_address, date_of_birth, passport, drivers_license, bank_account) with JS-identical type labels and redaction placeholders. It does not implement the Python SDK's regional/industry tier (Emirates ID, Aadhaar, ICD-10, etc.) — see Regional PII Detection (v1.1) below for the pre-existing, separate gap in that area.
Documentation
Visit tork.network for full documentation.