Skip to content

Invariants ​

Rules that break the product, or the project, when violated. Short on purpose: this file is meant to be read whole before touching src/Adapters, src/Io or the dependency list.

Everything here is enforced by a test, a tool, or an explicit review step.

Most of what used to be here is signet-pdf's now. Appending a revision rather than rebuilding, operating on the last match, never assuming whitespace in PDF syntax, parsing ASN.1 by declared length, leaving K_PATH_FONTS undefined: every one of those governs code that no longer lives in this repository. They are in signet-pdf's own invariants, and a change here cannot violate them (0039).

What follows is what a wrapper can still break.


1. The adapters are bound, always ​

Signet\Signet resolves its own defaults when nothing is injected: Support\SymfonyProcessRunner and Signing\Cades\HttpTransport. Both work. Both are wrong here.

A class built inline cannot be faked. If the provider stops injecting Adapters\IlluminateProcessRunner, Process::fake() silently stops covering the CLI certificate reader and the signature verifier, and no consuming application's suite reports it. The same holds for Adapters\IlluminateSignatureTransport and Http::fake().

This is the whole reason the package exists rather than a composer require line in the application, so a regression here is not a small one.

Enforced by tests/Console/CheckEnvironmentTest.php, which asserts that a faked process is what the environment check sees, and tests/Adapters/TransportTest.php, which asserts the same for HTTP.


2. Network access stays behind the injected transport ​

Signet\Contracts\SignatureTransport is the TSA, OCSP and CRL client. The host application owns that SSRF surface, and in a Laravel application owning it means Http::fake(), preventStrayRequests(), the application's proxy, its CA bundle, its middleware and its logging.

Nothing else in src/ opens a connection, and nothing may.

Enforced by tests/Adapters/TransportTest.php, including the case that says a pades-b-b signature reaches no network at all.


3. The config file holds scalars, and nothing else ​

config/a1-pdf-sign.php is read by Config\SignetConfigFactory and turned into Signet\Config\SignetConfig at resolution time.

A config file carrying an object cannot be cached. config:cache serialises the array; an enum instance or a readonly object fails there, and it fails in the consuming application rather than here, on a command nobody runs until deployment.

Enforced by tests/Container/ConfigTest.php.


4. Only Adapters\IlluminateProcessRunner spawns a child process ​

Every shell-out goes through the one audited adapter, built on Illuminate\Process\Factory. Two callers legitimately reach a process, both inside signet-pdf and both through the contract: the legacy PFX reader and the signature verifier.

Enforced by tests/Project/ArchTest.php (only the shell adapter opens processes).


5. Verification instruments never reach production ​

veraPDF, qpdf, pdfsig, pdftoppm, Ghostscript, pyHanko and Arlington's testgrammar are measuring instruments. Nothing in src/ may invoke one, and nothing built for testing may ship.

The rule stays even though the tools are no longer installed anywhere near this package: it is about what ships, not about what is present (0026).

Enforced by tests/Project/ArchTest.php and tests/Project/DistributionTest.php, which asks git archive what a release actually contains.


6. An agent reaches only what the application opened, and signs only with a person's approval ​

Two rules, one reason: an argument an agent tool receives was written by a model, and whoever writes the prompt steers the model (0040).

Every disk and path a model names goes through Agents\DocumentAccess. It refuses a disk missing from a1-pdf-sign.agents.disks, which is empty by default, an absolute path, .. anywhere, a file that is not a PDF, a document over agents.max_bytes, and a destination that exists. No agent tool builds a disk source itself.

The application's gate is asked before the document is looked at, when it defines Agents\Ability::Read or Agents\Ability::Sign, so a refusal never says whether a path exists (0041).

Ai\Tools\SignPdf always asks. shouldRequestApproval() returns an Approval for every call, withoutApproval() throws, and the tool takes no certificate or password from the model. A change that let a signature through without a person is not a feature request, it is this rule broken.

Enforced by tests/Agents/DocumentAccessTest.php, tests/Ai/SignPdfTest.php, tests/Ai/AgentLoopTest.php, which drives the SDK's own loop and asserts that nothing is written before approval, and tests/Project/ArchTest.php, which forbids the tools from naming Storage, the disk classes or the engine.


7. The SDKs stay optional ​

laravel/ai and laravel/mcp are suggestions. A class naming one cannot be loaded without it, so laravel/ai is named only in src/Ai, laravel/mcp only in src/Mcp, and nothing outside either directory names a class inside it. The provider registers nothing from them.

Enforced by tests/Project/ArchTest.php, and by the CI job that removes both SDKs and runs the suite without them.


8. PSR-4 autoloading is case-sensitive ​

InvalidX509PrivateKeyException has a capital X. A file named Invalidx509... autoloads on macOS and fails in production.

Version 3.2.0. Released under the MIT License.