Clean Framework · DOCUMENTATION
Opt-in native media, quarantine and worker monitoring
Opt-in native media, quarantine and worker monitoring
Bounded candidate reference — 2026-10-20: 0.2.0-rc.1 is committed locally through 84f415b3098af5653577b2c65eea31fca21df79c, unpublished and not production-approved; stable PHP >=8.5.11 <8.6.0 is required. This is documentation, not adoption: this knowledge base retains its installed 0.1.0-rc.1 dependency and PHP 8.2.11 local profile. Dated PHP 8.2 evidence is historical; earlier dirty-source reports retain their original test provenance, not retroactive clean-commit proof. Candidate examples are illustrative: API signatures were reviewed, but candidate code was not executed by this editorial update. See current checkpoint, open criteria and adoption boundary.
This supplemental implementation follows the owner's approval after D-020. It does not begin Phase E or activate a consumer application. Earlier phase reports describe their historical source; use this guide and the new closing receipt for the added code. The original release matrix is not reclassified.
Components and boundaries
| Component | Supplied behavior | Not supplied or implied |
|---|---|---|
VideoInspection / FfprobeVideoInspector | Strict local MP4 inspection including rational frame rate, container bit rate and stream counts | Malware verdict, object authorization or arbitrary formats |
FfmpegMediaProcessor | Closed-profile conversion, native output reinspection and no-replace private promotion | Public delivery, arbitrary flags, URLs, playlists, upscaling or automatic activation |
ProcessGroupMediaToolRunner | Allowlisted argument vectors, cleared environment, time/output bounds and same-session descendant cancellation | OS resource isolation or containment of a process that escapes its session |
PrivateWorkspace | Opaque keys, private ownership/modes, bounded staging and no-overwrite promotion | An authorization system or protection against a compromised process with the same UID |
ClamAvMalwareScanner | Actual local engine adapter; only exact clean success accepts bytes | Automatic database downloads, database freshness, production feed qualification or a guarantee that malware cannot pass |
QualifiedClamAvMalwareScanner / ClamAvDefinitionProfile | Opt-in exact definition pins and operator-selected finite qualification window checked before/after a scan | Vendor provenance, update downloads, automatic renewal or consumer activation |
FileQuarantine | Private held bytes, policy/audit hooks, fresh scan and inspection, holds, expiry and publication fencing | Distributed transactions, public download routes, clean override or automatic uncertain-outcome recovery |
HeartbeatQueueWorker / FileWorkerPulseStore / WorkerWatchdog | Idle and active-cycle pulses, incarnation fencing and independent stale detection through an injected alert sink | A process supervisor, real notification subscription, responder acknowledgement or shared cross-host state |
All bindings are explicit dependency injection. Existing unavailable adapters, constructors, metadata serialization, routes and module defaults are unchanged. Applications must adopt their own central module policy at every entry point; the presence of these classes is not an enabled module or permission to bypass authentication, object policy, CSRF, approvals, audit or retention controls.
Closed video profile
Only media.transcode, explicit mp4, positive even width/height no greater than 1920x1080, no upscaling, null quality and an opaque .mp4 output key are accepted. Source bytes must be at most 25 MiB, MP4/H.264 with exactly one video stream and zero or one AAC audio stream, at most 60 seconds, 60 fps and 20,000,000 container bits/second. Unknown, subtitle and data streams are denied. The richer inspection object is separate from the unchanged MediaMetadata.
FFmpeg uses fixed H.264/yuv420p/CRF23/fast, 30 fps, a 4,000,000 video maxrate with 8,000,000 buffer, optional AAC128k, one encoder/filter thread, aspect-fit scaling plus padding, no copied metadata/chapters, and a 25 MiB/60-second output cap. These settings are not a promise of exact byte-identical files across native builds or exact container bit rate. The output is probed again: exact requested dimensions, H.264/optional AAC, expected audio presence, at most 30.01 fps, the closed bounds and a duration difference no greater than 500 ms. Only then may a fresh private key be promoted. Existing outputs are not replaced.
Example composition inside an already isolated job, not an HTTP handler:
// The operator has created this canonical, non-public, UID-owned 0700 root.
// Never use a request value for the root, executable, transform or CLI flags.
$workspace = new \Clean\Framework\Storage\PrivateWorkspace('/work/private-job');
$runner = new \Clean\Framework\Media\ProcessGroupMediaToolRunner();
$inspector = new \Clean\Framework\Media\FfprobeVideoInspector($runner);
$processor = new \Clean\Framework\Media\FfmpegMediaProcessor($runner, $inspector, $workspace);
// Admission/authentication and immutable source acquisition precede this call.
$sourceKey = $workspace->import($authorizedLocalSourcePath);
$derivative = $processor->process(new \Clean\Framework\Media\MediaTransformRequest(
'media.transcode', $sourceKey, 'preview.mp4', 640, 360, 'mp4'
));
// This is still a private file, not a public URL or an authorization grant.
$privateDerivativePath = $workspace->file($derivative->reference());capabilities() reports executable presence only. It does not qualify the host, verify sandbox flags or perform a scan. All failures remain safe internal probe exceptions; do not expose raw exception chains, native stderr or paths.
Isolation and cancellation
The Linux non-root runner only executes canonical trusted executables, never a shell. It starts an owned setsid session, closes stdin, clears inherited application environment and bounds arguments, stdout, discarded stderr and wall time. It kills the owned process group and reaps its child on completion, timeout and output overflow. A descendant that creates another session is outside that PHP guarantee: the operator must terminate the entire disposable container, including on cancellation or runner failure.
The approved native test profile uses a pinned locally built image, network-none, UID10001, read-only root, dropped capabilities, no-new-privileges, 2 CPUs, 2 GiB RAM, 256 PIDs, no core dumps, 256 file descriptors, an init reaper, 128 MiB noexec tmpfs /tmp and 512 MiB noexec tmpfs /work. It never mounts the Docker socket inside the job. The host orchestrator checks ownership labels before removing exact IDs. These are measured fixture settings, not automatically imposed when an application constructs a PHP runner.
Production adoption still needs an approved supervisor/one-job container launcher, trusted read-only source, separate private output volume and namespace, minimum filesystem privileges, quotas, hard deadlines, whole-container cleanup, patched/pinned image and definition update policy. Do not colocate credentials, unrelated application state or quarantine index authority with an untrusted decoder. A same-UID private directory alone is not a hostile-code sandbox. No general production Docker launcher or deployment is enabled by this work.
Scanner and quarantine
Bind ClamAvMalwareScanner($runner, $trustedDefinitionPath) explicitly. The definition path must exist and be canonical/readable. The adapter does not run freshclam or verify feed age: an operator must enforce signature provenance, freshness, immutable mounting and update/requalification policy before intake. It limits file/scan/archive bounds, refuses encrypted/broken/limit-exceeding content, verifies source identity before/after and accepts only exit zero plus exact path: OK output. Detection, missing engine/database, execution failure, timeout, skipped or malformed responses all deny publication. Scanning is not a proof of safety or a replacement for upload inspection and access policy.
For the separately approved additive qualification contract, use the CF-SEC-Q03 scanner guide. Existing scanner constructors and freshness ownership are unchanged. The wrapper binds the adapter to the exact profile path and denies pin/identity/expiry failures; an old profile cannot renew itself merely because mtime or scanner CLEAN changed. Operators must first establish actual definition provenance and update policy.
FileQuarantine requires its own existing private workspace, this scanner, an UploadInspector, a PrivateStorage, QuarantinePolicy, AuditSink, Clock, explicit lowercase environment and finite capacity (maximum 1,000). Do not share its root with decoder jobs, pulse metadata or other services. The capture caller owns authentication, rate/size admission and acquisition of the authorized source. Capture copies bytes, retains digest/size, starts HELD and accepts retention from one second through 365 days. No route delivers the held copy. Audit writes are required before mutations/side effects.
Implement QuarantinePolicy::allows($actor, $action, $itemId) using the real authenticated identity and exact object policy. The actions are quarantine.inspect, quarantine.hold, quarantine.publish and quarantine.delete. Opaque IDs and actor grammar are not authentication.
Publication claims HELD -> SCANNING using a fresh owner, verifies captured digest/size, scans immediately, reinspects content, verifies identity again and records release intent. An expired or held item is denied, including at the final transition. A confirmed failure before publication can return to HELD; a later call must repeat all checks. Before calling private storage the claim becomes RELEASING. Success becomes RELEASED. An exception/crash or lost acknowledgement after that fence stays RELEASING: never automatically retry storage, reset to HELD, delete or declare it absent. Reconcile storage and audit evidence through a separately approved operator procedure. An index, audit sink and private storage are not one atomic transaction.
setHold() is independently authorized; a hold blocks publication and expiry cleanup. deleteExpired() removes only an expired, unheld quarantine copy in HELD or RELEASED. It does not delete a separately published object. An interrupted deletion stays DELETING for reconciliation. DELETED tombstones remain and count toward capacity; there is no automatic eviction, archive or namespace rotation. At capacity stop intake safely and obtain an approved retention/reconciliation design. The local atomic index is not an NFS, multi-node or power-loss durability guarantee.
Idle worker and independent watchdog
Use a different private 0700 root for FileWorkerPulseStore. Compose the same named service and store into an opt-in HeartbeatQueueWorker and into a separately supervised process running WorkerWatchdog::check():
// Use a fixed, low-cardinality application service name, never a user ID.
$pulses = new \Clean\Framework\Observability\FileWorkerPulseStore($pulseWorkspace, $clock);
$loop = new \Clean\Framework\Queue\HeartbeatQueueWorker($queueWorker, $pulses, 'media.worker');
// The wrapper admits one existing queue-worker attempt per cycle and pulses
// before/after it, including idle cycles; it changes no job/outbox authority.
$report = $loop->run(['media'], 100, 100000);
// In a separate process: inject the application's selected alert sink.
$watchdog = new \Clean\Framework\Observability\WorkerWatchdog(
$pulses, $clock, new \Clean\Framework\Observability\AlertManager($alertSink, $clock),
['media.worker' => 180]
);
$health = $watchdog->check();Choose the accepted age above the longest handler duration plus scheduling jitter; the example 180 seconds is not a universal default. There is no background heartbeat that could hide a stuck handler. A supervisor owns restarts and signals; requestStop() drains the admitted attempt and stops future claims. Cycles are bounded to 10,000, idle delay to one second; a finite loop exit requires supervisor action, not an eternal implicit daemon.
Each worker incarnation gets a new owner; stale owners cannot update the new pulse. This fence is monitoring metadata, not a distributed worker lock. At most 128 named services are stored. Missing/stale/future/corrupt timestamps are unhealthy, even with queue depth zero. The watchdog emits fixed critical codes through the injected sink; sink exceptions propagate. AlertManager cooldown is process-local, so one-shot watchdog invocations need application shared deduplication/delivery policy. An in-memory alert is not a real email, operator response, independent channel or acknowledgement.
Verification and remaining qualification
Run bin/isolated-acceptance --gate=native only with the explicit local Docker socket and reviewed native image ID recorded in BUILD_LOG.md. The gate requires its native-image label and cannot pull/build an image. The dedicated Dockerfile is a local tooling recipe, not a runtime dependency or release artifact. Package downloads occurred only in the separately approved build; all acceptance runs are offline.
Actual local tests cover authored video with/without AAC, padded dimensions, truncated MP4 rejection, no overwrite/upscale, benign signature controls using the actual ClamAV engine, descendant cancellation, output bounds, kernel settings and an actually stopped idle worker observed by a separate process. Contract tests cover modes/aliases, denied scanning/policy, holds/expiry, tampering, finite capacity and lost publication acknowledgement.
The scanner test database is an authored harmless hash control, not a production definition feed, EICAR or malware corpus. Broad hostile/corrupt codec/resource-exhaustion corpora, current CVE/license review, production feed updates, actual host/restart/fault drills, real alert delivery/acknowledgement, consumer adoption and independent/human reviews remain unqualified. No original criterion is promoted and no feature is automatically enabled.
Native command reference: FFmpeg, ffprobe and ClamAV scanning.
Editorial provenance
Source: docs/native-operations/DEVELOPER_GUIDE.md. Source SHA-256: a30513c0381d9f8651866a0730beca7dcdaa229da587f022b997e861562dccba.
Framework candidate reference; editorial checkpoint 2026-10-20. See candidate status and adoption boundary.