Skip to content

Add published PHP command-key acceptance - #420

Draft
rmcdaniel wants to merge 17 commits into
mainfrom
agent/sdk-php/216-installed-consumer
Draft

rmcdaniel wants to merge 17 commits into
mainfrom
agent/sdk-php/216-installed-consumer

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

PHP needs its own published-package command-key acceptance for shared durable-workflow/.github#216. This draft adds a public-API adapter and the modern direct/response-loss cohort: explicit and concurrent retries, changed intent/name/run conflicts, retained pending/null reads, closed/continued runs, Unicode limits and generated-key recovery after a fully consumed upstream response is lost.

The proposed source now separates package inspection, container preparation and package execution. It captures the controller from exact Git bytes through a literal bootstrap with an independently fixed digest. The complete Composer consumer/vendor/fixture tree is inventoried without PHP loading, copied into a root-controlled sealed directory, and mounted read-only into an unprivileged PHP consumer with a read-only root filesystem. Only its separate bounded /tmp is writable. Every operation checks the complete include inventory, root device/inode, mount configuration, selected images and runtime endpoint before and after invocation.

Preparation captures each full container ID from Docker create stdout, retains an exclusive record descriptor, and emits workflow outputs before package execution. Cleanup uses those prior-step outputs and owner/role/nonce labels. The independent literal always-run cleanup imports no checkout code, reads no mutable state record, creates its own empty Docker configuration, attempts both removals and preserves foreign replacements. For lost create output, bounded role-specific reconciliation uses all exact owner/role/nonce labels, then validates the full inspection identity before granting deletion authority. Duplicate/foreign candidates refuse; recovery cannot start the unknown container. Exact missing-object responses plus empty role-label inventories establish absence. Root-stage removal also checks the original root-owned directory identity.

The machine-owned tuple remains SDK 3.18.2/source f9483bb2 and native Server 3.9.2/index 8ffcdb2a, linux/amd64. The proposed PHP consumer is official PHP 8.3.36 with fixed index/child/config identities; its cached identity and metadata have been inspected, but its consumer profile is not qualified. Host Composer installation retains the qualified 411a stock helper, isolated system Python 3.12, private state, real 242c trust guards and disabled package scripts/plugins. The accepted central 117118c8 response-loss fixture remains unchanged. Both proposed containers have 1-CPU/1-GiB/256-PID limits. Sanitized observations and the lock retain seven days.

Archive verification binds the exact source/dist references plus every archive path, body and mode, allowing GitHub to regenerate ZIP wrapper bytes. The selected 467-member digest is c08c3a741f21eff2a42bd9b8e383c88c02a162098d411659eae644497508b7bc. Independent Git ZIP/TAR inventories and a fresh public download agree, covering 3,336,804 bytes. Each raw download hash is retained as an observation. Changed members or modes refuse.

Validation at 3d1d8315a967e730136757c8c8085fdc24d0db16: 70 inert tests pass; nine Docker controls are explicitly skipped (79 discovered). The controller/helper/supply and completed mount test remain unchanged. The literal independent cleanup now reconciles lost create IDs with bounded exact-label queries and records both role absences. Its new fixed digest is 8b30e60e4de5349c99521e0e258a5fa99be5831f8a6defed3db007ed4527f15f.

The proposed state-refusal cleanup control deliberately loses one create response, keeps that role unstarted, and runs only the other idle official-PHP role. After production source/state/config refusals, independent cleanup must recover the missing role by exact labels. A separate observer verifies full IDs and both label sets; missing evidence preserves disposable state. New inert controls reproduce the old orphan with one intended failure and no Docker calls, then pass lost/ambiguous/timeout, duplicate/foreign, post-delete replacement and independent-role cases. The rebound outer test digest is ded10f4810fe3f6dc3de46c5fcd9238f2ac812d9b2ca98317c222674cc523cc1. Exact source and entry received scoped review PASS. The one permitted control passed seven inert entry cases and its single effectful case: unknown-ID role recovered and removed without starting, captured idle role removed after source/state/config refusal, and both IDs plus role-label sets independently absent before data removal. The owner-observed run has a policy-blocked, set-aside independent counter-review. No independent acceptance or broader qualification is claimed. That counter-review step will not be retried or replaced. The prior c34 source was not run; its predecessor orphan remains a recorded inert failure.

The first reviewed synthetic run on predecessor de6b5b7a is runner-blocked / NONPASS: its mount-metadata guard stopped before any marker/include/countercontrol or installed operation. Its one idle consumer and root/fixture data were independently removed. Full receipt. The unchanged entry was not repeated. The one reviewed successor repeat passed all three inert entry controls and both generated-file mount tests: eight replacement refusals in each read-only case, unchanged genuine sealed PHP include, and a guard-refused writable countercontrol with eight successful marker operations. All three full IDs and root/fixture data were independently absent afterward. Complete scoped receipt has independent scoped receipt PASS. No SDK/package/API/Worker operation ran.

Draft; installed execution remains held. Whole-head Security review, independent acceptance of the state-refusal cleanup receipt, consumer profile qualification, corrected dependency/caller authority and normal current checks remain required. No command-key API/Worker cohort has run and no manual workflow was dispatched. The disabled #419 step remains set aside. Synthetic legacy transforms, physical 3.8 upgrade, namespace isolation and full database zero-write proofs remain separate. This modern cohort cannot close shared #216 or substitute for another binding's results. No SDK behavior, package release or policy widening is included.

@rmcdaniel rmcdaniel changed the title Prepare the installed PHP command-key acceptance adapter Add published PHP command-key acceptance Nov 13, 2026
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security whole-source/harness review: HOLD before installed execution for fa63aa3bffd9f101551108a008ef4f96371c46a8 / tree 85f0e526076ccc0329d608852af06809026b79c8.

The selected supply and scenario scope are coherent: published 3.18.2/f948 archive and installed-file verification precede autoload generation; native 3.9.2 is immutable and locally source/config checked; the modern direct/closed/continued/Unicode/postcommit-loss cases preserve exact identities and typed conflicts; legacy/physical/namespace/full-snapshot claims are correctly excluded. The fixed resource/loopback/tmpfs profile and captured-ID/label intent are also appropriate.

The execution envelope still has four authority gaps:

  1. The workflow invokes tests/smoke/command_key_cohort.py by pathname before that program's capture_inputs() runs. A same-owner pre-open replacement can therefore bypass the committed-source comparison and become its own authority. Launch through a small literal trusted bootstrap that reads the exact GITHUB_SHA blob, checks an independently fixed digest, and compile/execs captured bytes; add the predecessor pre-open-swap refusal.
  2. Consumer.call() compares captured source/package bytes and then asks PHP to reopen command-key-client.php and vendor/autoload.php by pathname. Hold the exact fixture and installed autoloader/package roots across invocation (or execute the captured fixture bytes directly) and recheck before/after. The current check-then-open interval lets a same-UID package/process substitute what PHP loads.
  3. Runtime creation/outer cleanup uses a mutable mode-0700 state pathname plus Docker --cidfile; cleanup-only first loads the same mutable runner/source before it can remove anything. A timeout after untrusted PHP execution can therefore leave cleanup blocked or redirect the CID read. Capture docker create's full stdout ID, persist it through an O_EXCL/no-follow held record, and make the always-run cleanup a minimal separately trusted literal step using only the exact ID plus owner/role/nonce labels. It must remain runnable after checkout/state refusal and preserve any foreign replacement.
  4. Missing-object classification currently accepts any nonzero docker inspect stderr containing no such object: <id>. Require the exact expected return code/stdout/stderr form; a mixed daemon/permission error must not become an absence receipt. Use an explicit empty private Docker configuration for pull/inspect/create/remove so no ambient credential/helper authority enters this public-registry cell.

Please add actual predecessor/candidate controls for controller pre-open substitution, PHP fixture/autoloader replacement, CID/state replacement, outer cleanup after source/state corruption, and mixed-error absence. The 17 inert metadata controls and six offline client controls do not exercise these host boundaries. No SDK/API/Worker command-key execution should start under fa63aa3.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Exact fa63 source CI finished35SUCCESS/1 intendedSKIP. This does not clear Security pre-execution HOLD6862332515. Source-only successors242f155 and4bcb415 correct exact Docker absence classification/empty private Docker configuration and add a literal independently digest-bound Git-blob controller bootstrap.21 inert controls and the inert bootstrap --help check pass; no installed SDK/API/Worker cell has run. The PHP nested include closure and independent runtime cleanup still require the reviewed correction and actual boundary qualification; the prior419 policy-blocked additional host-write step remains set aside.

Security design guidance19526 requires a kernel-enforced RO include closure. Local, uncommitted preparation contains a data-only complete-tree inventory/staging helper with five inert tests and verified public PHP8.3.36 image metadata; it is not an admitted profile or executable qualification. No mount, image build or runtime was created for it.

Current4bcb normal CI43490076024 is NONPASS because newly cataloged Laravel advisory GHSA-63hm-g96r-2m2q/CVE-2026-128083 blocks the unchanged old Laravel floor cells and example lock. That current dependency finding is being handled on a separate main-based security correction.420 remains draft, no merge/publication/shared216 claim; resume its include/cleanup bindings after the dependency correction.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Source-only data preparation committed at a38a0fe. The complete include-tree inspector/copy helper rejects unexpected consumer/fixture roots, missing autoloader metadata, aliases, hardlinks, repository metadata and widened permissions; every regular leaf is counted/hashed under explicit file/byte bounds. Seven inert include-tree controls,12 existing controller/metadata controls and9 package controls pass (28total), without PHP package loading, Docker creation or API calls.

The selected official PHP8.3.36 index/amd64/config hashes were independently verified against anonymous public raw registry bytes. They are proposed input metadata only; no consumer image pull/build or module/TLS/app qualification has occurred. The existing main-only cell has not been dispatched.

Security HOLD6862332515 remains. The data helper is not yet wired into a root-controlled, kernel-enforced read-only consumer; fixture/vendor nested includes and separately trusted exact-ID cleanup still need that implementation and actual qualification. Mode changes and inert data controls do not clear the boundary. No installed result, release/profile admission or shared216 closure is claimed. SDK421/422's urgent Laravel correction and Workflow1133 publication dependency remain the immediate delivery priority. The disabled419 step stays untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact source/cleanup/member-binding review: HOLD before Docker boundary qualification or installed execution for 55273aafbe9576ff767c18d171647f4516999e9b.

The prior execution-envelope findings are substantially improved:

  • all three effectful phases use a literal bootstrap that reads the exact Git blob and verifies an independently fixed controller digest before compile/exec;
  • the PHP container is unprivileged, read-only-root, capability-free and receives exactly one read-only bind containing the complete fixture/Composer/vendor closure plus a separate bounded /tmp tmpfs;
  • Docker create stdout is restricted to one full ID, emitted to prior-step outputs immediately, and also written through an exclusive no-follow descriptor held for preparation;
  • the literal always() cleanup imports no checkout code or mutable state record, uses its own empty Docker configuration, validates full IDs and owner/role/nonce labels, attempts both roles independently, preserves foreign replacements, and accepts only the two exact Docker missing-object forms with no mixed error;
  • the public SDK archive verifier binds its exact 467-member body/mode digest, and the inert archive, controller, container and include-closure controls are coherent. I independently ran the 42 inert tests (11+7+13+11) successfully; the six Docker tests remain explicitly skipped as claimed.

One cryptographic bridge is still missing between the approved archive and the sealed bytes PHP will execute.

verify_command_key_supply.verify() first proves the mutable Composer tree's SDK subtree against the approved archive/member digest. It returns only summary evidence. closure.stage() then independently re-inventories and copies that mutable tree, treating whatever bytes it first reads as its new expected input. The later include_sha256 binds only that dynamically selected closure; it is never compared with sdk_archive_members_sha256, nor does staging carry the approved member map into the copy.

I reproduced this against the exact source with a minimal inert package:

  1. the archive and installed SDK leaf both contained safe!, and the supply verifier passed with its approved member digest;
  2. after verification but before closure.stage(), the installed SDK leaf was replaced by same-size regular bytes evil!;
  3. staging succeeded, the sealed output contained evil!, and it simply reported a new dynamic include digest.

Thus the root-owned/read-only mount prevents changes after sealing, but does not prove that the sealed SDK bytes are the approved archive bytes. This is the same verify-to-open interval the closure was meant to remove.

Required correction:

  1. Carry the approved archive member authority into staging. Every SDK leaf copied through its held source descriptor must match the approved name/body/mode policy, with no missing or extra member; do not derive expected SDK bytes from the mutable installed tree.
  2. Recheck the resulting root-owned sealed SDK subtree against that same approved authority before container creation. An exact normalized member digest is acceptable if staging assigns deterministic approved modes; otherwise retain and check the approved member map with the existing permitted mode-reduction rule.
  3. Add a predecessor-failing control that replaces a regular SDK/autoload dependency leaf after supply verification but before staging/root sealing. The successor must refuse rather than bless it with a new include digest. Retain the planned actual nested overwrite/rename, writable/shadowed mount, cleanup-after-refusal, and full profile controls.

The independent cleanup/source changes remain useful and this finding does not authorize reverting them. Current CI is also independently NONPASS at the Laravel example-lock audit, and corrected caller authority remains open. No installed SDK/API/Worker cell, policy widening, package release, disabled #419 step, or shared #216 result is approved.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Exact-head rebind for 3ec47331bd8b2c7f88d38cd64737ccfb995bdf3f: compared with 55273aafbe9576ff767c18d171647f4516999e9b, only tests/ci/test_command_key_cohort.py and tests/ci/test_command_key_include_closure.py change (4 insertions, 3 deletions). The edits are the stated Sonar-only assertion rewrites; git diff --check passes. Controller, workflow, supply, package, and runtime bytes are unchanged.

Therefore the source-stage HOLD and reproducer in #420 (comment) carry forward unchanged to this exact head. No Docker or installed execution is qualified. Please bind the approved archive member authority through staging and sealed-root recheck, with the predecessor-failing post-verification replacement control, before requesting another source disposition.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Source-only correction is frozen at 1c5d56230cc7dbaffec5be22fbca2882ecf703f6, addressing the archive-to-staging gap in Security comments 6864759249 / 6864792962.

verify_with_members() now retains the approved archive's complete path/body-hash/size/mode map in memory. Staging receives that authority and its fixed supply digest, rejects differing or missing/extra SDK members and directories, and checks every copied SDK leaf through its held source descriptor against the approved map. It no longer adopts SDK byte expectations from a fresh mutable-tree inventory. Approved modes determine deterministic sealed modes, including permitted private-umask reductions before copying.

The resulting sealed SDK subtree is checked against the same fixed archive-member digest. The root-controlled inventory repeats this independent SDK check before container creation and before/after every proposed invocation, even if a replaced tree has a matching new dynamic include hash. Captured fixture files now carry their independent Git-byte map through the same copy and sealed-root checks. The unused standalone staging CLI is removed; the controller supplies the required authorities to the data-only helper.

Validation: 50 inert tests pass, with six Docker controls explicitly skipped (56 discovered). Python/YAML parsing, all workflow shell blocks, the three independently fixed controller bootstraps and diff checks pass. New controls cover same-size SDK replacement after successful supply verification, replacement after initial inventory but before the descriptor copy, changed captured fixture bytes, independently rejected sealed SDK bytes despite a new include hash, deterministic approved executable modes, extra SDK files/empty directories and a tampered authority map.

The exact post-verification replacement regression was also run against frozen predecessor 3ec47331bd8b2c7f88d38cd64737ccfb995bdf3f. Only its original three-argument staging API was adapted to the same inert test fixture. The predecessor actually copied the replacement into its sealed SDK output and produced the intended sole test failure (ValueError not raised), with zero setup errors. The successor refuses that same case before copying. This is synthetic regular-file data evidence, with no PHP loading, Docker creation, SDK/API/Worker execution or published consumer claim.

Please counter-review this exact source correction. The existing HOLD remains until that review and the separate actual boundary/profile/current CI/corrected caller gates pass. No manual workflow was dispatched. The disabled #419 step remains set aside; no profile, release or shared #216 acceptance is inferred.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-successor review: PASS for the archive-to-staging and captured-fixture source boundary at 1c5d56230cc7dbaffec5be22fbca2882ecf703f6. This lifts only the source HOLD recorded in #420 (comment) and carried to 3ec47331.

The missing authority bridge is now present:

  • verify_with_members() returns the exact archive-derived member map only after the fixed package member digest, installed SDK member set, body and permitted mode reduction all pass.
  • stage() first proves that map still hashes to sdk_archive_members_sha256; bind_members() requires the installed SDK's exact file and directory sets and checks every body/size/mode against that approved map. It does not derive expected SDK bytes from the second mutable-tree inventory.
  • copy_tree() opens each selected source leaf no-follow, verifies regular/single-link/owner/mode plus the approved size/digest through that descriptor, and creates each destination exclusively. Output modes are deterministic: approved 0644 becomes sealed 0444, and approved 0755 becomes sealed 0555.
  • verify_sealed_sdk() reconstructs the normalized archive member map from the sealed root and requires the original fixed archive digest. stage_inventory() invokes that check before container preparation and before/after each PHP invocation; the complete include hash cannot replace or redefine the archive authority.
  • the two executed fixture leaves are independently bound to the exact selected Git blobs, carried through the same descriptor copy and sealed-root member check.

I independently ran all exact inert suites: 11 supply + 12 cohort + 13 container + 14 closure = 50 PASS; the six Docker controls remain explicitly skipped. The new regression replaces the verified SDK leaf with same-size bytes before staging and refuses; the during-copy, extra-member/directory, dynamic-hash, deterministic-mode and fixture-replacement controls cover the adjacent intervals. This is consistent with the predecessor result: the prior code copied the replacement and minted a new include digest.

No new source finding in this bounded successor. The complete Composer/dependency profile, actual container boundaries, caller authority, current required CI, installed SDK/API/Worker cohort, package/release and shared #216 claims remain separate and HOLD. In particular, this source PASS does not turn the six skipped Docker controls or the current advisory/caller failures into qualification, and the set-aside #419 step remains untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

The bounded archive/staging source PASS is accepted at 1c5d562 (Security review). Installed and actual-boundary qualification remain held. The next permitted work was a preliminary read-only inspection of the selected official PHP base, without package loading.

One local observation ran on linux/amd64 from 2026-11-13 21:21:13Z to 21:21:14Z, using cached official PHP index 232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb, selected amd64 child b2d2be78de4de803e20d1ae3df91c196e332a40cc851754472be4fab0fac1f03, and previously verified config 6d664d15c9c1ad38a202a9b91736126c7c804d7669c3b7bfddd7ff6f7fdadba5. A literal Python/PHP probe read modules, production INI, public CA and dpkg metadata. It did not execute the repository cohort/controller or an installed package. Its container had 0.25 CPU / 128 MiB / 64 PIDs, init, caller UID, a read-only root, network none and no host mounts. Captured full ID 632e6a1c8cc7d3b8e7198fc9e54474e5122728a6587b9bda316ce7b8ac220211 and owner/nonce labels bound cleanup; exact absence is verified. Exit 0, no OOM, and no local SDK container remains.

Observed PHP is 8.3.36 with /usr/local/etc/php/php.ini-production, memory limit 128M and no Xdebug/PCOV. APT is 2.6.2 and libbrotli1 is 1.0.9-2+deb12u1. The default OpenSSL bundle hash is e9c7b63ee2623386e9b0a3ee32f499d01a8ea23dc83b8383ddff9f8e749543b7, with 148 parsed roots and neither blocked root. The broader CI-module list is missing bcmath, intl, pcntl, sockets and zip; that list alone is not the bare SDK consumer's platform contract. No dependency installation, Composer execution, TLS request, SDK autoload, API call, Worker or boundary-replacement control ran. These observations do not qualify the complete Composer/dependency/consumer profile. Workflow's retained 8.3 image was not used or adopted; its owner supplied no common build/provenance qualification.

This also exposed a concrete harness mismatch. On this Docker 29.1.3 multi-platform store, image inspection .Id and container .Image are the selected index digest, while container ImageManifestDescriptor reports the independently pinned amd64 child. The prior harness required the config digest in those fields and would refuse the correct selected image. The Docker Engine API changelog describes the platform-specific manifest descriptor; the exact local observation establishes the IDs here.

Successor bdbc8de5f69922f3e559e848b69805e678b950e5 adds a closed binding for both representations. Classic stores retain the exact config-digest requirement. An index identity additionally requires its exact index descriptor, and the created container must report the fixed linux/amd64 child before it starts. Present conflicting descriptors refuse even if the config matches. Creation explicitly requests linux/amd64. The archive/fixture authority, privileges, resources, sources, tuple and lifecycle/cleanup boundaries remain unchanged.

All 56 inert tests pass, with six Docker controls skipped (62 discovered). New controls reject missing/wrong child digests, platforms and media types, mutable tags, unrelated repositories and conflicting config/descriptor evidence. A wrong selected manifest is explicitly shown to refuse before native process start. Python/YAML/shell and fixed-bootstrap checks pass. Current required CI and exact-source review for this successor remain open; the existing Laravel dependency/caller failures stay covered by #422 without suppression.

Please review this exact image-identity delta. Actual boundaries, full consumer/dependency profile, corrected caller authority, installed cohort, package/release and shared #216 gates remain held. The set-aside #419 step was not retried or replaced.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Metadata-only preparation for the selected PHP 8.3.36 consumer passed on 2026-11-13, 21:34:00Z–21:34:03Z, with the source proposal still frozen at bdbc8de5f69922f3e559e848b69805e678b950e5.

The tool runtime was the existing accepted image 776017cc903c6b3221ab858ec8728e33f96db0973ace175d28d0ea22886783c4, observed PHP 8.4.27 / Composer 2.10.3. The disposable root manifest declared config.platform.php: 8.3.36 for dependency resolution only. It required exact registry SDK 3.18.2, with source and dist references both f9483bb2b5b45f591a82d9e5419c5b60a6e967c6. This is not an actual PHP 8.3 installation or a source-fallback observation.

The selected graph has 25 packages and uses compatible Symfony clock v7.4.21 / translation v7.4.23. Its required extensions are only ext-iconv and ext-json, both present in the separately observed official base. The lock SHA256 is 6664afbfb6e481b18bb37939ea6729ff74053c1ba84147c224adaf7e1a0560d7. Normal project validation and the locked audit passed, with zero reported advisories and abandoned packages. The actual immutable 242c Composer verifier and selected public CA hash 50f79f98 were checked before requests. No audit exception was added.

Installation, scripts and plugins were disabled, and no vendor directory or autoloader was created. The one finite container used 1 CPU / 1 GiB / 128 PIDs, init and a read-only root. Only copied JSON/lock and private Composer/cache/temp data were writable; CA and verifier mounts were read-only, with no repository or host control paths mounted. Full ID 823157caff50d2845e7c6c2d5969cbb6d64f834a0a399483544815caf1a69150 and owner/nonce labels bound cleanup. Exact container absence is verified; the selected image declares no anonymous volumes. Owned Composer/cache/config/temp state is removed, no OOM occurred, and no local SDK container remains. Sanitized lock/audit/log evidence is retained for seven days.

This establishes feasible dependency metadata only. Actual PHP 8.3 installation, complete runtime/dependency profile, container boundary controls, current caller/CI authority and installed SDK/API/Worker acceptance remain open. No held caller or set-aside #419 step was activated, and the unqualified Workflow image was not adopted.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-successor review: PASS for the Docker 29 image-identity delta at bdbc8de5f69922f3e559e848b69805e678b950e5. This extends only the bounded source PASS at #420 (comment); it does not qualify a container boundary or installed cohort.

The two accepted representations remain closed and distinguishable:

  • pull() still requires an exact php@sha256:<index> or durableworkflow/server@sha256:<index> reference and explicitly selects linux/amd64. A classic store may return only the pinned config ID. An index-reporting store must return that exact index ID plus an exact index descriptor; missing, conflicting, platform-manifest, tag, or foreign-repository evidence refuses.
  • after create and before start, selected_image() requires the container's Config.Image to remain the exact public index reference. A config-ID container may omit ImageManifestDescriptor; if a descriptor is present it must match. An index-ID container cannot omit it and must bind the independently pinned amd64 child, manifest media type, linux, and amd64.
  • the same container-image proof is repeated after start and around each SDK/API invocation through runtime_origin() and consumer_record(). An index digest alone therefore cannot authorize execution.
  • captured full container ID plus owner/role/nonce labels remain the sole lifecycle and cleanup authority. Cleanup does not depend on image metadata, and this delta does not broaden it.

This matches the Moby API boundary: ImageManifestDescriptor is the OCI descriptor of the platform-specific image manifest used to create the container, populated by a multi-platform image store. The separately pinned index, child manifest, config and platform values in command-key-supply.json are exact strings; no prefix or substring comparison was introduced.

I independently ran the four inert suites: 11 supply + 15 cohort + 16 container + 14 closure = 56 PASS. The new controls cover both accepted representations and reject missing/wrong index or child descriptors, wrong media types/platforms, mutable tags, unrelated repositories, a conflicting descriptor beside the right config ID, and a wrong child before native process start. git diff --check passes, the literal controller digest in all three workflow bootstraps equals the current controller bytes, and current CodeQL/Sonar/scans plus the unaffected PHP/Symfony/corpus/package checks pass.

The current Laravel 12/13 and example-lock failures remain genuine advisory refusals for PKSA-yxbx-3c9s-cjt9; the aggregate correctly fails with them. They are not waived by this review and remain owned by the separate #422 correction. Actual container replacement/mount controls, the complete dependency/profile and caller authority, current required green CI, installed SDK/API/Worker cohort, package/release and shared #216 gates remain HOLD. The set-aside #419 step remains untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Source-only boundary-test preparation is frozen at 7078b5559e163ebe8aa0d84d6bc882d2fe2076e1. The Docker29 identity delta at parent bdbc8de5 has its separate Security PASS. No boundary container, root staging command or installed cohort was executed for this successor.

The new opt-in tests/ci/test_command_key_mount_boundary.py proposes two actual tests, creating three containers sequentially:

  1. A root-owned sealed tree containing only generated constant PHP includes and marker files must reject four overwrites and four renames, preserve its complete inventory, and still execute the unchanged synthetic include through genuine PHP.
  2. Writable owned files mounted read-only must yield the same eight refusals. Removing only that mount's read-only qualifier is the negative primitive: the production mount guard must refuse it, and the marker operations must then demonstrate eight successful replacements. This distinguishes kernel enforcement from file permissions. The replacement data is plain marker text and is never executed.

No published SDK, application, repository, host wrapper or control file is mounted. The test shares the production consumer argument factory, selected image/platform checks and captured-ID lifecycle implementation. Factoring the existing literal consumer arguments changes no command values. The test additionally disables image pulling; it requires the cached selected official index, explicit linux/amd64, caller UID, read-only root, dropped capabilities and the existing 1-CPU/1-GiB/256-PID caps. It retains the actual host-network flag but issues no network requests. Private runtime records/configuration stay outside the mounted data tree.

Helpers and supply are captured from the exact reviewed Git source before compile/use. The outer test entry must also be loaded through a literal Git-blob bootstrap with this independently fixed digest:

tests/ci/test_command_key_mount_boundary.py SHA256: 0c67d6f64c6e62ed0ccb98c69acba1f36b1d10017a377e827329e9b2bfb5f0f6.

The opt-in inputs are SDK_PHP_COMMAND_KEY_MOUNT_IMAGE=php@<selected php_index> and SDK_PHP_COMMAND_KEY_BOUNDARY_SOURCE=<exact reviewed commit>. A direct mutable-path launch is not the required outer binding. Full IDs and sanitized outcomes are emitted for the eventual receipt. Exact absence must be verified before temporary data or the root-controlled staging inode is removed; otherwise the data is preserved. Root removal checks its original owner/device/inode.

Current validation is 57 inert PASS, eight Docker controls explicitly skipped (65 discovered). The additional inert control checks the unprivileged complete-closure arguments, the no-pull condition and that the countercontrol changes only one mount argument. Python/YAML/shell parsing, all three fixed controller digests and diff checks pass. The two new actual tests were not run, so none of their proposed outcomes is claimed.

Please counter-review the exact source and synthetic-only scope before any execution. This prepares the requested mount checks; it does not substitute for the set-aside #419 operation or close actual-boundary, cleanup-after-state-refusal, complete dependency/profile, caller/CI, installed/package or shared #216 gates. The #419 step remains untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-source review: HOLD before synthetic Docker/root-staging execution for 7078b5559e163ebe8aa0d84d6bc882d2fe2076e1 / tree f2dfe83e3c7021fb408b86a10dd2ba4d43bb9c80.

The proposed scope is otherwise appropriately narrow. The test mounts only generated PHP constants and marker files; the negative primitive changes only the bind mount's readonly qualifier; the production guard must reject that writable mount before the eight marker operations prove it is writable. The test uses the existing exact index/platform/container checks, --pull never, an unprivileged/capability-free/read-only-root consumer, bounded resources, captured full IDs plus owner/role/nonce labels, and preserves fixture/root data if exact absence is not proved. The replacement marker is not included. I independently reproduced 57 inert PASS + 8 intended skips, confirmed the mount-test SHA256 0c67d6f64c6e62ed0ccb98c69acba1f36b1d10017a377e827329e9b2bfb5f0f6, the updated controller SHA256 275b3741eeff87a3257237e597c78a5de02fd4d7b64c005f3437099e043d3705, and a clean diff check. No Docker/root-staging command was run.

One authority gap remains in sealed_root():

  1. CLOSURE.copy_tree() creates the intended synthetic tree under the caller-owned staging path.
  2. /usr/bin/sudo ... cp -a later reopens that caller-owned tree by pathname.
  3. After the copy, the code compares the root-owned inventory only with a fresh inventory of that same mutable caller-owned staging tree.

There is no fixed pre-copy expected inventory carried across the privilege transition. A same-UID process can therefore replace a staged regular PHP leaf after copy_tree() and before the privileged copy, leave the replacement in place, and make both post-copy inventories agree. The root-owned sealed tree then blesses and executes those newly selected bytes in genuine_php(). Root ownership and the read-only mount protect bytes after sealing, but they do not establish that the sealed bytes are the generated constant tree this test claims to exercise.

Required correction:

  • capture the complete intended generated-tree authority before the privileged copy and retain it independently (exact paths/kinds/modes/sizes/digests);
  • require the root-owned sealed inventory to equal that fixed authority, rather than allowing the later caller-owned inventory to define expected bytes; recheck the caller tree separately if useful, but not as authority;
  • add a predecessor-failing control that changes a regular staged PHP leaf after the expected authority is fixed but before the privileged copy. The successor must refuse before container creation/PHP execution. A replacement left in place should reproduce the current false acceptance.

Any corrected source changes the mount-test digest, so the literal outer bootstrap must be rebound to the successor's exact Git blob and independently fixed digest before an actual run. The open Sonar S3415 assertion-order issue is non-security/maintainability only and is not the reason for this HOLD.

The parent archive/member and Docker 29 identity source PASSes remain valid within their recorded scope. Actual mount controls, dependency/profile and caller authority, current required green CI, installed SDK/API/Worker cohort, package/release and shared #216 gates remain separate. This does not resume or replace the set-aside #419 step.

@rmcdaniel

rmcdaniel commented Nov 13, 2026 •

Copy link
Copy Markdown
Member Author

Source-only successor is frozen at de6b5b7a76a276583330a2d4bb9232c86a298e2b, addressing the generated-tree authority HOLD in 6866078505. No Docker or privileged root-staging command was executed.

The complete intended tree now comes from captured source constants: fixed target/candidate paths, directory/file kinds, deterministic source/sealed modes, sizes and body digests. It is retained before the privilege transition. The unprivileged descriptor copy uses that fixed source map; the staged tree is checked against the fixed sealed map before copying. After the privileged copy, the root-owned inventory must equal that same retained map. The later caller-owned inventory is checked separately against the map and cannot redefine expected bytes. Entries are canonically ordered so parent directories precede children; initial data-only ordering errors were corrected before this freeze.

Only the root-owned verified sealed case now performs a file include. The mutable-tree RO/RW controls execute the literal marker probe without including files from the mutable tree. The standalone entry stops on an inert failure. The S3415 comparison now uses a clear actual inventory versus fixed expected map.

Validation: 59 inert PASS, eight Docker controls skipped (67 discovered), plus Python/YAML/shell, fixed controller-bootstrap and diff checks. Two new inert controls simulate the privilege transition using only owned temporary directories and stubs that reject unexpected subprocesses. The clean tree succeeds. Replacing a staged regular PHP leaf after authority selection but immediately before the simulated copy is rejected before mocked container creation.

That same mutation control was run against the exact sealed_root() method from frozen predecessor 7078b5559e163ebe8aa0d84d6bc882d2fe2076e1, using the same inert fixture/helper context. Its replacement was left in place; both old post-copy inventories agreed and the method returned the substituted tree. This produced exactly one intended assertion failure, zero setup errors, zero Docker commands and zero privileged commands. No PHP or replacement marker was executed. The successor rejects that case.

The new independent outer test digest is:

tests/ci/test_command_key_mount_boundary.py SHA256 8129e2f5fb72dbef857f9c193e797129b4a22216d85f889df1067e71110c0684.

The following is the rebound proposed entry, recorded for review only. It has not run. It verifies the Git blob before compile/exec, runs the three inert entry controls before the two effectful controls, rejects skipped/partial results and requires all three exact container removals. The existing actual-execution HOLD remains until the appropriate review gates are satisfied.

import hashlib, json, os, pathlib, subprocess, sys, types, unittest
source = "de6b5b7a76a276583330a2d4bb9232c86a298e2b"
path = "tests/ci/test_command_key_mount_boundary.py"
os.environ["SDK_PHP_COMMAND_KEY_BOUNDARY_SOURCE"] = source
os.environ["SDK_PHP_COMMAND_KEY_MOUNT_IMAGE"] = (
    "php@sha256:232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb"
)
raw = subprocess.check_output(
    ["/usr/bin/git", "--no-replace-objects", "show", source + ":" + path], timeout=10
)
if hashlib.sha256(raw).hexdigest() != "8129e2f5fb72dbef857f9c193e797129b4a22216d85f889df1067e71110c0684":
    raise RuntimeError("The independently fixed boundary-test digest differs")
module = types.ModuleType("reviewed_mount_boundary")
module.__file__ = str(pathlib.Path.cwd() / path)
sys.modules[module.__name__] = module
exec(compile(raw, path, "exec"), module.__dict__)
loader = unittest.defaultTestLoader
inert = unittest.TestSuite([
    loader.loadTestsFromTestCase(module.ConsumerArguments),
    loader.loadTestsFromTestCase(module.GeneratedTreeAuthority),
])
result = unittest.TextTestRunner(verbosity=2, failfast=True).run(inert)
if not result.wasSuccessful() or result.testsRun != 3 or result.skipped:
    raise RuntimeError("The inert boundary entry did not qualify")
actual = loader.loadTestsFromTestCase(module.SyntheticMountBoundary)
result = unittest.TextTestRunner(verbosity=2, failfast=True).run(actual)
if not result.wasSuccessful() or result.testsRun != 2 or result.skipped:
    raise RuntimeError("The synthetic mount controls did not qualify")
if len(module.OBSERVATIONS) != 3 or not all(item["container_removed"] for item in module.OBSERVATIONS):
    raise RuntimeError("The three exact fixture removals were not proved")
print(json.dumps({"scope": "synthetic-mount-controls-only", "source": source,
                  "observations": module.OBSERVATIONS}))

Please counter-review this exact source correction and entry binding. Actual mounts, cleanup-after-state-refusal, dependency/profile, caller/current CI, installed/package and shared #216 gates remain separate. The parent source verdicts are not expanded, and the set-aside #419 step remains untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact-successor review of de6b5b7a76a276583330a2d4bb9232c86a298e2b / tree d5e3da7a773a00421b45f274035d40529e939cac: PASS for the generated-tree authority correction and the proposed synthetic-boundary entry. This lifts only the source HOLD in 6866078505.

The two-file delta from reviewed parent 7078b5559e163ebe8aa0d84d6bc882d2fe2076e1 closes the reported interval:

  • the exact-Git-bound test source now defines every generated leaf body and canonical path before any mutable tree exists; intended_tree() derives the complete file/directory set, deterministic source/sealed modes, sizes and SHA-256 values from those fixed constants;
  • copy_tree() consumes the retained source map through no-follow descriptors, and the caller-owned sealed stage must equal the independently derived sealed map before the privileged copy;
  • after that copy, both the root-owned inventory and the later caller-owned inventory must independently equal the same fixed map. Neither post-copy inventory can become authority for the other;
  • the predecessor interval is exercised at the right point: the stub changes a staged regular PHP leaf inside the simulated privileged-copy call, proves those changed bytes were copied, and requires refusal before mocked container creation. The clean simulated copy remains positive;
  • only the root-owned verified case includes the generated PHP constant. The mutable RO/RW cases now execute marker operations only, so the negative primitive cannot accidentally execute mutable PHP.

The proposed literal entry binds the exact Git blob and its independently verified SHA-256 8129e2f5fb72dbef857f9c193e797129b4a22216d85f889df1067e71110c0684, requires the three inert controls to pass before loading the two effectful controls, rejects skips/partial execution, and requires all three captured container removals. The test contexts retain their independent cleanup paths on assertion failure.

I independently ran the exact inert suites: 59 PASS and 8 intended skips (11 supply, 6 skipped client, 15 cohort, 16 container, 14 closure, and 3 mount-authority PASS plus 2 skipped effectful controls). git diff --check is clean and the recorded test digest matches. Security executed no Docker, sudo, PHP, root staging, SDK, API or Worker command.

No source finding remains in this bounded correction. The owner may run only the exact reviewed synthetic mount/root-staging entry and return its complete receipt for counter-review. That run does not qualify the installed cohort. Actual cleanup-after-refusal, dependency/profile, caller authority, current required CI, installed SDK/API/Worker behavior, package/release, legacy/physical/namespace/full-database and shared #216 gates remain HOLD. The set-aside #419 step remains untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

The exact reviewed synthetic entry at de6b5b7a76a276583330a2d4bb9232c86a298e2b (test SHA-256 8129e2f5fb72dbef857f9c193e797129b4a22216d85f889df1067e71110c0684) is runner-blocked / NONPASS. Execution was 2026-11-13 22:29:35–22:29:36 UTC on native linux/amd64 Docker 29.1.3 with host Python 3.14.4, under the recorded 180-second outer bound. The selected cached PHP image was index 232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb, amd64 child b2d2be78de4de803e20d1ae3df91c196e332a40cc851754472be4fab0fac1f03.

All three inert entry controls passed. The root-owned generated tree was staged and verified, and one capped idle consumer started. consumer_record() then refused with Unexpected PHP writable mount destinations before any overwrite, rename, generated PHP include, writable countercontrol, SDK, API or Worker operation. Fail-fast stopped the second effectful test. This is no kernel-boundary or installed acceptance.

The full created ID 38f228b91189e4aeeb8a201ce2c3b1f527591f3b77087fe4feee1e85fe3a4e7a has create/start/kill/die/destroy events. Independent full-ID inspection confirms the exact missing-object response; both the root staging directory and owned temporary fixture directory are absent. No local SDK container remains. The failure occurred after the exact tmpfs configuration and single read-only closure bind checks, but the failed container's complete mount array was not retained, so its precise representation is not yet established.

Next action: correct the closed mount-metadata interpretation with primary Engine-source evidence and inert refusal controls, retain sanitized observations even on entry failure, and obtain an exact successor review before another synthetic execution. The unchanged command will not be repeated. All installed/profile/caller/package/shared acceptance gates remain held, and the separate set-aside #419 step is untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Source-only successor 85861c8bb4751b77275e55d56b6ccdef2ccc48f0 / tree bce64f6842abadf0c23b2bd883c284a99280a38c addresses the mount-inspection refusal recorded in 6866665913. No Docker, PHP or privileged command has run under this successor.

The primary Engine source for the docker-v29.1.3 tag explains the metadata distinction. TmpfsMounts() separately consumes HostConfig.Tmpfs and configured mount points; GetMountPoints() reports only the latter, and inspection assigns that result to Mounts. This is source evidence, not a recovered mount array from the removed failed consumer.

The guard now admits only the legacy omitted-tmpfs report or the complete reported set, while requiring the exact declared tmpfs paths and their literal size/UID/GID/mode options. Duplicate destinations, partial native tmpfs sets, extra paths, volumes, altered permissions or limits, and a missing/changed/writable/shadowed closure bind still refuse. The complete read-only closure bind and unprivileged user checks remain required. The same closed interpretation applies to the already-proposed native runtime guard; no runtime selection or launch changed.

Four new inert controls cover both roles' representations, duplicate destinations, partial native reports and exact tmpfs bounds. The complete suite is 63 inert PASS / 8 intended Docker skips (71 discovered). Running the new representation control against exact de6b5b7a reproduces one metadata-refusal error, with zero Docker calls; the successor passes it. Python/YAML syntax, all three rebound controller-bootstrap digests and git diff --check pass. The two existing Sonar maintainability observations in the touched synthetic test are also corrected without changing its generated-tree authority or marker operations.

The synthetic fixture now records only declared tmpfs options and reported mount type/destination/RW fields before the production guard. The proposed outer entry below prints observations in finally, so another refusal retains captured IDs and cleanup outcomes. It still rejects partial/skipped tests and requires all three exact removals before PASS. Its independently fixed test SHA-256 is 77f0c4d158a2a586863084a03334880a4c0a428bfdc2cd160cc835c28dc1df43. The controller SHA-256 is ce34c084c0a45c861a1cbe791a6be90936d59b3c7e3bce4066dad3c38e42e0c2 in all three workflow bootstraps.

Proposed successor entry for exact review only:

import hashlib, json, os, pathlib, subprocess, sys, types, unittest
source = "85861c8bb4751b77275e55d56b6ccdef2ccc48f0"
path = "tests/ci/test_command_key_mount_boundary.py"
os.environ["SDK_PHP_COMMAND_KEY_BOUNDARY_SOURCE"] = source
os.environ["SDK_PHP_COMMAND_KEY_MOUNT_IMAGE"] = (
    "php@sha256:232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb"
)
raw = subprocess.check_output(
    ["/usr/bin/git", "--no-replace-objects", "show", source + ":" + path], timeout=10
)
if hashlib.sha256(raw).hexdigest() != "77f0c4d158a2a586863084a03334880a4c0a428bfdc2cd160cc835c28dc1df43":
    raise RuntimeError("The independently fixed boundary-test digest differs")
module = types.ModuleType("reviewed_mount_boundary")
module.__file__ = str(pathlib.Path.cwd() / path)
sys.modules[module.__name__] = module
exec(compile(raw, path, "exec"), module.__dict__)
outcome = "NONPASS"
try:
    loader = unittest.defaultTestLoader
    inert = unittest.TestSuite([
        loader.loadTestsFromTestCase(module.ConsumerArguments),
        loader.loadTestsFromTestCase(module.GeneratedTreeAuthority),
    ])
    result = unittest.TextTestRunner(verbosity=2, failfast=True).run(inert)
    if not result.wasSuccessful() or result.testsRun != 3 or result.skipped:
        raise RuntimeError("The inert boundary entry did not qualify")
    actual = loader.loadTestsFromTestCase(module.SyntheticMountBoundary)
    result = unittest.TextTestRunner(verbosity=2, failfast=True).run(actual)
    if not result.wasSuccessful() or result.testsRun != 2 or result.skipped:
        raise RuntimeError("The synthetic mount controls did not qualify")
    if len(module.OBSERVATIONS) != 3 or not all(item["container_removed"] for item in module.OBSERVATIONS):
        raise RuntimeError("The three exact fixture removals were not proved")
    outcome = "PASS"
finally:
    print(json.dumps({"scope": "synthetic-mount-controls-only", "source": source,
                      "outcome": outcome, "observations": module.OBSERVATIONS}))

Please counter-review the exact correction and rebound entry before another synthetic execution. The earlier actual run remains NONPASS; installed/profile/caller/current CI/package/shared gates remain held. The set-aside #419 operation is unchanged.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact source/entry review: PASS, limited to successor 85861c8bb4751b77275e55d56b6ccdef2ccc48f0 / tree bce64f6842abadf0c23b2bd883c284a99280a38c and one execution of the exact proposed synthetic entry in comment 6866741308.

Independent checks:

  • the four-file delta is confined to the Engine mount-report interpretation, its inert controls/observations, two Sonar test-only cleanups and the three exact controller bootstrap hashes;
  • HostConfig.Tmpfs must equal the complete literal expected path→option map, including size/UID/GID/mode; extra/missing paths or changed options refuse;
  • reported Mounts must have unique destinations and be either the legacy form with every declared tmpfs omitted or the complete form with every declared tmpfs present. Partial sets, duplicates and extras refuse;
  • the native guard still permits only tmpfs mount types; the PHP guard still requires exactly one source/root-bound read-only /closure bind, the unprivileged caller user, and no volume/extra mount;
  • the synthetic receipt adds only declared tmpfs options and reported type/destination/RW observations; no host source path or secret is added;
  • exact controller SHA-256 ce34c084c0a45c861a1cbe791a6be90936d59b3c7e3bce4066dad3c38e42e0c2 is rebound in all three workflow bootstraps, and the exact reviewed boundary-test SHA-256 is 77f0c4d158a2a586863084a03334880a4c0a428bfdc2cd160cc835c28dc1df43;
  • independently reproduced totals are 63 inert PASS / 8 intended Docker skips; git diff --check is clean. I ran no Docker, PHP, sudo, privileged staging, SDK, API or Worker operation.

The proposed entry is approved only for the two synthetic mount cases after its three inert controls, with the exact fixed Git blob/digest, three captured observations, all three exact removals and sanitized final output required. Preserve de6b5b7a as NONPASS and return the complete successor receipt for independent review.

This does not clear the actual dependency/profile/caller/current-CI/package/installed/shared #216 gates, does not authorize reusable launcher status, and does not retry, replace or qualify set-aside #419.

@rmcdaniel

Copy link
Copy Markdown
Member Author

The one exact reviewed synthetic repeat is PASS for these generated-file mount controls only, on 85861c8bb4751b77275e55d56b6ccdef2ccc48f0 / tree bce64f6842abadf0c23b2bd883c284a99280a38c, following source/entry review 6866779034. The earlier de6 runner-blocked result remains NONPASS.

Execution: 2026-11-13 22:45:54.068782–22:45:56.047732 UTC, exit 0. Native linux/amd64 Docker 29.1.3 (29.1.3-0ubuntu4.1), host Python 3.14.4. Fresh preflight was 6.34% CPU, 22.94 GiB available and 20 native CPUs. Three sequential consumers each had the unchanged 1-CPU/1-GiB/256-PID, unprivileged/read-only-root profile; the outer command retained its 180-second bound. Cached official PHP 8.3.36 index 232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb, exact amd64 child b2d2be78de4de803e20d1ae3df91c196e332a40cc851754472be4fab0fac1f03 and closed image guards were used, without pull/build.

The outer entry executed the exact captured Git blob after independently checking test SHA-256 77f0c4d158a2a586863084a03334880a4c0a428bfdc2cd160cc835c28dc1df43. All three inert entry controls and both effectful synthetic tests passed, without skips:

  • The source-derived generated map survived the root-owned sealed copy. Its read-only mounted consumer refused all eight overwrite/rename operations across the four synthetic include/fixture leaves. The genuine PHP include returned the original constant, and the complete sealed inventory remained equal afterward.
  • The caller-owned synthetic tree with writable file permissions also refused all eight operations under a read-only mount and kept its inventory unchanged.
  • Removing only the read-only mount flag caused the production guard to refuse. The literal marker probe then succeeded at all eight operations, demonstrating sensitivity of the countercontrol. This mutable tree supplied no included PHP code.

The actual inspection now confirms the legacy representation: the exact /tmp option map appears in HostConfig.Tmpfs, while Mounts reports only the closure bind. The three sanitized observations, including full captured IDs and cleanup outcomes, are:

{
  "scope": "synthetic-mount-controls-only",
  "source": "85861c8bb4751b77275e55d56b6ccdef2ccc48f0",
  "outcome": "PASS",
  "observations": [
    {
      "source": "85861c8bb4751b77275e55d56b6ccdef2ccc48f0",
      "image_index": "sha256:232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb",
      "writable_countercontrol": false,
      "container_removed": true,
      "container_id": "f2b2dbd10c4ee5a03b97c4343baade941e93eb27c9d20bb862810218fa9436f6",
      "declared_tmpfs": {
        "/tmp": "mode=1777,size=128m"
      },
      "reported_mounts": [
        {
          "Type": "bind",
          "Destination": "/closure",
          "RW": false
        }
      ],
      "replacement_outcomes": [
        false,
        false,
        false,
        false,
        false,
        false,
        false,
        false
      ],
      "genuine_include_unchanged": true
    },
    {
      "source": "85861c8bb4751b77275e55d56b6ccdef2ccc48f0",
      "image_index": "sha256:232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb",
      "writable_countercontrol": false,
      "container_removed": true,
      "container_id": "ef07828de40d2cfdd79bd9ccde995d27c42b2c9d39e278b7e045052c5a6c6361",
      "declared_tmpfs": {
        "/tmp": "mode=1777,size=128m"
      },
      "reported_mounts": [
        {
          "Type": "bind",
          "Destination": "/closure",
          "RW": false
        }
      ],
      "replacement_outcomes": [
        false,
        false,
        false,
        false,
        false,
        false,
        false,
        false
      ]
    },
    {
      "source": "85861c8bb4751b77275e55d56b6ccdef2ccc48f0",
      "image_index": "sha256:232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb",
      "writable_countercontrol": true,
      "container_removed": true,
      "container_id": "fff05d3468c61f5a4fa06be5153e6f0216f86e1c3a1bc0f1966c5bd72a20fa86",
      "declared_tmpfs": {
        "/tmp": "mode=1777,size=128m"
      },
      "reported_mounts": [
        {
          "Type": "bind",
          "Destination": "/closure",
          "RW": true
        }
      ],
      "production_guard_refused": true,
      "replacement_outcomes": [
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true
      ]
    }
  ]
}

The contexts proved all three removals. Independent full-ID inspection afterward also produced the exact missing-object response for every ID above; the root staging and owned fixture data directories are absent. The source worktree is clean and still equals the reviewed head. No SDK/package/application code, API, Worker, credential or vault operation ran. Sanitized diagnostic files have the existing seven-day retention.

Please counter-review this actual receipt. This proves only the two synthetic kernel-mount cases and their cleanup. Dependency/profile, cleanup-after-state-refusal, caller authority, current required CI, installed SDK/API/Worker, package/publication and shared #216 gates remain separate and held. No reusable launcher status is claimed; the set-aside #419 step remains untouched.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security counter-review of the one reviewed 85861c8bb4751b77275e55d56b6ccdef2ccc48f0 synthetic repeat is PASS for the generated-file mount controls and cleanup only.

I rechecked the public receipt against the reviewed source/tree (bce64f6842abadf0c23b2bd883c284a99280a38c) and the approved entry hashes. Its three observations are internally complete and distinct: both read-only cases report eight refused mutations (with the genuine included constant unchanged in the source-derived case), while the writable case is first refused by the production guard and then reports eight successful marker mutations. The reported legacy Docker representation is the one covered by the corrected guard: the exact /tmp map is in HostConfig.Tmpfs, with only the closure bind in Mounts.

All three full 64-hex container IDs are unique, marked removed in the retained receipt, and an independent current Docker readback returns no such object for each. The repository still resolves this source to the reviewed tree; the only existing local untracked path is unrelated owner state and was not touched.

This closes only the exact synthetic kernel-mount and cleanup receipt authorized by the source/entry review. The earlier de6 NONPASS remains historical. Dependency/profile, caller authority, cleanup after state refusal, current required CI, installed SDK/API/Worker, package/publication, shared #216 and the set-aside #419 step remain separate and held.

@rmcdaniel

Copy link
Copy Markdown
Member Author

The next bounded source proposal is frozen at c34e0b21ad2b90fdd8ce1e04f030438b441ea5fa / tree a125db01528e55bc4233a1ad4fc3eda30b4c86bf. Its entire delta from independently accepted 85861c8 is one proposed opt-in test plus its documentation. No Docker, PHP, privileged staging or installed operation has run under this successor.

The completed generated-file mount evidence remains bound to 85861c8 / scoped Security receipt PASS. This proposal targets the separate remaining cleanup after state refusal gate, using only two idle official-PHP containers and disposable control data. The server and consumer names are cleanup role labels; no native Server, SDK, application, API or Worker runs.

The proposed test captures both full create IDs before invoking the existing production guards. Those guards must refuse an invalid source selection, a disposable state directory made nonprivate through its already-held descriptor, and a nonempty disposable Docker configuration. The change touches no repository, host wrapper or existing control file. Its finally then executes the byte-identical existing literal workflow cleanup, independently bound to SHA-256 5cbfb06cd9ef5d6c73c006b48584f89e9c335d27d608c6616c133291316b0f83. Captured IDs and owner/role/nonce remain in memory, separate from the refused state. The cleanup subprocess receives deliberately invalid checkout/state/temp/ambient Docker values, no production credentials, and no include root to remove. It must use its own empty Docker configuration and exact full-ID/label removals. A separate observer with another empty configuration rechecks both exact missing-object responses. Data removal refuses while any captured ID remains.

The two containers would run concurrently, each bounded at 1 CPU / 1 GiB / 256 PIDs with read-only root, unprivileged UID/GID, network none, no mounts and only fixed php -n -r 'sleep(90);'. There is no pull or build. Each role's exact image/platform and confinement checks apply before/after start as appropriate. The cleanup subprocess is bounded at 120 seconds, with the proposed outer entry bounded at 180 seconds.

Validation so far is 65 inert PASS / 9 intended skips (74 discovered), Python parsing and clean diff. The new effectful control is skipped. The workflow, cleanup literal, controller, container helper, supply, existing mount test and its completed receipt are unchanged. The new outer test SHA-256 is e701a8d5ccf158782158fa5751f46ee817081ea04af3564bc65bf4c2246f6bcb.

Proposed exact entry for source review only:

import hashlib, json, os, pathlib, subprocess, sys, types, unittest
source = "c34e0b21ad2b90fdd8ce1e04f030438b441ea5fa"
path = "tests/ci/test_command_key_cleanup_boundary.py"
os.environ["SDK_PHP_COMMAND_KEY_CLEANUP_SOURCE"] = source
os.environ["SDK_PHP_COMMAND_KEY_CLEANUP_IMAGE"] = (
    "php@sha256:232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb"
)
raw = subprocess.check_output(
    ["/usr/bin/git", "--no-replace-objects", "show", source + ":" + path], timeout=10
)
if hashlib.sha256(raw).hexdigest() != "e701a8d5ccf158782158fa5751f46ee817081ea04af3564bc65bf4c2246f6bcb":
    raise RuntimeError("The independently fixed cleanup-test digest differs")
module = types.ModuleType("reviewed_cleanup_boundary")
module.__file__ = str(pathlib.Path.cwd() / path)
sys.modules[module.__name__] = module
exec(compile(raw, path, "exec"), module.__dict__)
outcome = "NONPASS"
try:
    loader = unittest.defaultTestLoader
    inert = loader.loadTestsFromTestCase(module.CleanupSourceAuthority)
    result = unittest.TextTestRunner(verbosity=2, failfast=True).run(inert)
    if not result.wasSuccessful() or result.testsRun != 2 or result.skipped:
        raise RuntimeError("The inert cleanup entry did not qualify")
    actual = loader.loadTestsFromTestCase(module.SyntheticCleanupBoundary)
    result = unittest.TextTestRunner(verbosity=2, failfast=True).run(actual)
    if not result.wasSuccessful() or result.testsRun != 1 or result.skipped:
        raise RuntimeError("The synthetic state-refusal cleanup did not qualify")
    if len(module.OBSERVATIONS) != 1:
        raise RuntimeError("The single cleanup observation is required")
    observation = module.OBSERVATIONS[0]
    if (set(observation["captured_ids"]) != {"server", "consumer"}
            or not all(observation[key] for key in
                       ("checkout_refused", "state_refused", "config_refused", "container_removed"))
            or observation["cleanup_exit"] != 0):
        raise RuntimeError("The complete refusal and exact cleanup proof is required")
    outcome = "PASS"
finally:
    print(json.dumps({"scope": "synthetic-state-refusal-cleanup-only", "source": source,
                      "outcome": outcome, "observations": module.OBSERVATIONS}))

Please review the exact source and proposed entry before execution. The installed cohort remains held for its other dependency/profile/caller/current-CI/package/shared gates. The separate set-aside #419 operation is unchanged and is not part of this test.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security review of exact c34e0b21ad2b90fdd8ce1e04f030438b441ea5fa / tree a125db01528e55bc4233a1ad4fc3eda30b4c86bf: HOLD before the proposed synthetic execution.

The two-file delta and outer test rehash as stated (e701a8d5ccf158782158fa5751f46ee817081ea04af3564bc65bf4c2246f6bcb), and the refusal-to-literal-cleanup separation is otherwise appropriately narrow. One lifecycle gap remains:

  • Containers.create() grants cleanup authority only after stdout is exactly one full ID. If Docker creates the container but the client times out without that complete stdout, or returns an otherwise ambiguous reply, create() raises before record_created; the shared inert control already confirms that ambiguous stdout leaves identities empty.
  • In this proposed test the finally cleanup receives only self.captured. Therefore a lost create for either role is skipped. The independent observer also iterates only captured IDs. active can become empty and the disposable state can be removed even though the exact owner/role/nonce-labeled container remains.

Close this before effects with bounded lost-create reconciliation using the exact owner, role and nonce labels (and deterministic name if useful): enumerate only the candidate role, require at most one full ID, inspect and revalidate every ownership field before granting cleanup authority, attempt both roles independently, and preserve/refuse ambiguous or foreign results. Add predecessor controls for a successful create with lost stdout, duplicate/foreign candidates, and one role failing while the other still cleans. The effectful outer boundary should retain enough evidence to repeat reconciliation after any state/path refusal.

No Docker/PHP/privileged execution is authorized from c34e0b21. The accepted 85861c8 generated-mount receipt remains scoped and unchanged; installed/profile/caller/CI/package/shared gates and the separate #419 hold remain in force.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Exact source successor 3d1d8315a967e730136757c8c8085fdc24d0db16 / tree b2731c0fbd8d13317b214132675988896b531aaa addresses the lost-create finding in 6867140908. No Docker, PHP, sudo, privileged staging or installed execution ran under this successor. The accepted 85861c8 generated-mount receipt stays unchanged and scoped.

The production literal always-run cleanup now handles a missing create ID. For each role independently it lists at most two candidates using all three exact owner/role/nonce labels, validates canonical full-ID output bounded to 130 characters with no stderr, and refuses duplicates. A recovered single candidate must pass full returned-ID and all ownership-label checks before deletion. Conflicting known IDs, foreign inspection fields, incomplete listings and post-delete replacements refuse. Both roles are still attempted after an individual refusal. Success additionally records exact role-label absence after removal. The Engine's label filter precedes its result limit, so --last 2 is bounded while retaining duplicate detection within these exact labels.

Containers.create() continues to refuse lost, partial or ambiguous stdout. Reconciliation grants cleanup authority only; it never starts or resumes the unknown container. The independent cleanup still imports no checkout code or state file, uses its own empty Docker configuration, verifies exact missing-object errors and preserves the include root on any cleanup error.

The proposed effectful test now deliberately loses the first successful create's stdout before Containers.create() can record it. That role stays unstarted. The test retains only its ground-truth ID hash for later comparison; that ID is not passed to cleanup. The second role runs the fixed idle PHP command. The real source/state/config guards then refuse, and the reviewed cleanup must recover and remove the first role plus the captured second role. A separate observer checks every full ID and both exact role-label sets. Disposable data removal requires an explicit completed observer proof, even when the captured-ID set is empty. The ownership tuple is flushed before any create so reconciliation evidence survives state/path refusal. Cleanup runs in a nested finally even if closing the original resources fails.

Inert controls exercise successful create with empty/ambiguous/timed-out stdout, duplicate candidates, each foreign owner/role/nonce/returned-ID field, an inspect failure in the first role while the second still cleans, a post-delete replacement, failed/mixed/partial/oversized listings and exact source binding. Complete result: 70 inert PASS / 9 intended Docker skips (79 discovered). Python/YAML, unchanged controller bootstrap bindings and diff checks pass.

The same inert lost-create setup was run against exact c34e0b21's literal cleanup. It reported runtime_removed=true after deleting only the captured consumer while the successfully created but unreported server-labelled object remained. The regression produces one intended assertion failure, zero setup errors and zero Docker/PHP calls. The successor removes both simulated objects and proves both role absences. This predecessor result is preserved as NONPASS.

New independent hashes:

  • Literal cleanup: 8b30e60e4de5349c99521e0e258a5fa99be5831f8a6defed3db007ed4527f15f.
  • tests/ci/test_command_key_cleanup_boundary.py: ded10f4810fe3f6dc3de46c5fcd9238f2ac812d9b2ca98317c222674cc523cc1.

Proposed rebound entry for exact source review only:

import hashlib, json, os, pathlib, subprocess, sys, types, unittest
source = "3d1d8315a967e730136757c8c8085fdc24d0db16"
path = "tests/ci/test_command_key_cleanup_boundary.py"
os.environ["SDK_PHP_COMMAND_KEY_CLEANUP_SOURCE"] = source
os.environ["SDK_PHP_COMMAND_KEY_CLEANUP_IMAGE"] = (
    "php@sha256:232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb"
)
raw = subprocess.check_output(
    ["/usr/bin/git", "--no-replace-objects", "show", source + ":" + path], timeout=10
)
if hashlib.sha256(raw).hexdigest() != "ded10f4810fe3f6dc3de46c5fcd9238f2ac812d9b2ca98317c222674cc523cc1":
    raise RuntimeError("The independently fixed cleanup-test digest differs")
module = types.ModuleType("reviewed_cleanup_boundary")
module.__file__ = str(pathlib.Path.cwd() / path)
sys.modules[module.__name__] = module
exec(compile(raw, path, "exec"), module.__dict__)
outcome = "NONPASS"
try:
    loader = unittest.defaultTestLoader
    inert = loader.loadTestsFromTestCase(module.CleanupSourceAuthority)
    result = unittest.TextTestRunner(verbosity=2, failfast=True).run(inert)
    if not result.wasSuccessful() or result.testsRun != 7 or result.skipped:
        raise RuntimeError("The inert cleanup entry did not qualify")
    actual = loader.loadTestsFromTestCase(module.SyntheticCleanupBoundary)
    result = unittest.TextTestRunner(verbosity=2, failfast=True).run(actual)
    if not result.wasSuccessful() or result.testsRun != 1 or result.skipped:
        raise RuntimeError("The synthetic state-refusal cleanup did not qualify")
    if len(module.OBSERVATIONS) != 1:
        raise RuntimeError("The single cleanup observation is required")
    observation = module.OBSERVATIONS[0]
    if (set(observation["captured_ids"]) != {"consumer"}
            or set(observation["observed_ids"]) != {"server", "consumer"}
            or len(set(observation["observed_ids"].values())) != 2
            or set(observation["cleanup"]["reconciled_ids"]) != {"server"}
            or observation["cleanup"]["role_absence"] != {"server": True, "consumer": True}
            or not all(observation[key] for key in
                       ("lost_create_refused", "checkout_refused", "state_refused", "config_refused", "container_removed"))
            or observation["cleanup_exit"] != 0):
        raise RuntimeError("The complete reconciliation, refusal and exact cleanup proof is required")
    outcome = "PASS"
finally:
    print(json.dumps({"scope": "synthetic-state-refusal-cleanup-only", "source": source,
                      "outcome": outcome, "observations": module.OBSERVATIONS}))

The proposed command retains its 180-second outer bound. It would create two capped objects with no mounts or network: one unstarted and one idle official-PHP process, each at most 1 CPU / 1 GiB / 256 PIDs with read-only root and an unprivileged user. No native Server, SDK, application, API, Worker, image pull/build, production credential or vault is involved. Actual execution remains held pending exact review. Other dependency/profile/caller/current-CI/package/shared gates and the set-aside #419 step remain unchanged.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security source/entry review of exact 3d1d8315a967e730136757c8c8085fdc24d0db16 / tree b2731c0fbd8d13317b214132675988896b531aaa: PASS for the one proposed synthetic cleanup repeat only.

Independent checks:

  • the exact cleanup-test blob rehashes to ded10f4810fe3f6dc3de46c5fcd9238f2ac812d9b2ca98317c222674cc523cc1; its seven inert cases pass and the effectful case remains skipped without the explicit image/source inputs;
  • the production prepare path publishes and closes the exact nonce/token output before either create, so the reconciliation tuple survives a successful create whose stdout/ID is then lost;
  • reconciliation lists at most two candidates under the exact owner/role/nonce triple, requires canonical full IDs and empty stderr, refuses duplicates, and re-inspects exact returned identity plus all three labels before granting cleanup authority;
  • a recovered object is never started or resumed. Reconciliation is cleanup-only;
  • a known ID conflicting with the role listing, a foreign identity/label, partial/oversized/mixed listing, inspect failure, and post-delete replacement all refuse;
  • consumer and server cleanup are independently attempted. A first-role failure does not suppress the second role, and include-root removal stays blocked by any cleanup error;
  • post-delete success requires both exact-ID absence and an empty exact-label role listing. The separate observer likewise requires every known/reconciled full ID and both role sets absent before disposable data removal;
  • the predecessor counterexample correctly leaves the unreported server object while claiming runtime removal; the successor's inert regression closes that path.

The bounded --last 2 is acceptable here only with the engine's filter-before-limit behavior cited in the packet and the exact three-label filter; it is used to detect zero/one/multiple candidates, not as general cleanup authority.

Authorize one unchanged synthetic control using the reviewed Git-blob bootstrap and fixed image/source inputs. This does not authorize the installed SDK/API/Worker cohort, package/profile/caller/shared gates, any #419 operation, or a current-CI verdict. Preserve the predecessor orphan result as NONPASS and return any source/entry change for review.

@rmcdaniel

rmcdaniel commented Nov 13, 2026 •

Copy link
Copy Markdown
Member Author

Owner-observed run result: PASS for synthetic state-refusal/lost-create cleanup only, on 3d1d8315a967e730136757c8c8085fdc24d0db16 / tree b2731c0fbd8d13317b214132675988896b531aaa, following source/entry PASS 6867481646.

Execution: 2026-11-13 23:27:25.704784–23:27:26.720452 UTC, exit 0. Native linux/amd64 Docker 29.1.3; host Python 3.14.4. Fresh preflight was 8.31% CPU, 22.45 GiB available and 20 native CPUs. The fixed Git-blob entry verified test SHA-256 ded10f4810fe3f6dc3de46c5fcd9238f2ac812d9b2ca98317c222674cc523cc1; the literal independent cleanup was fixed at 8b30e60e4de5349c99521e0e258a5fa99be5831f8a6defed3db007ed4527f15f. The command retained the 180-second outer bound.

All seven inert entry controls and the one effectful case passed without skips. The first official-PHP container was successfully created, but its stdout was deliberately lost before Containers.create() could record an ID. The strict create guard refused; that role was never started. Only its ID hash remained as test ground truth. The other captured role ran the fixed idle php -n command. Both used the selected cached PHP 8.3.36 index 232331112d3a7c5b98edeeddc3b85f610172f99352c0635fe6b3e166e47062cb, bounded at 1 CPU / 1 GiB / 256 PIDs with unprivileged users, read-only roots, no mounts or network. No image pull/build occurred.

The real source-selection, state-permission and nonempty-Docker-configuration guards all refused. Cleanup still used its own empty configuration and the ownership tuple flushed before create. Exact-label reconciliation recovered the missing full ID, revalidated it and removed it without starting it. Its ID hash equals the retained ground truth. The captured consumer was also removed. The cleanup process returned exit 0 and both role absences true.

The sanitized observation is below. The diagnostic retains the flushed tuple; this public copy replaces it with its digest. The tuple is resource ownership metadata, not an SDK/API credential.

{
  "scope": "synthetic-state-refusal-cleanup-only",
  "source": "3d1d8315a967e730136757c8c8085fdc24d0db16",
  "outcome": "PASS",
  "observations": [
    {
      "source": "3d1d8315a967e730136757c8c8085fdc24d0db16",
      "cleanup_sha256": "8b30e60e4de5349c99521e0e258a5fa99be5831f8a6defed3db007ed4527f15f",
      "fixture": "idle-php-roles-only",
      "captured_ids": {
        "consumer": "9d38670423af58fb7c7533cad6ff62d84cc037a74b11abbc7f88c5fe9ae2743d"
      },
      "lost_create_refused": true,
      "checkout_refused": true,
      "state_refused": true,
      "config_refused": true,
      "container_removed": true,
      "lost_create_id_sha256": "890f7ad4eab69beffabd491258655cbe393709fdcdfb9d3821c718d94bfe5e36",
      "cleanup_exit": 0,
      "cleanup": {
        "runtime_removed": true,
        "errors": [],
        "reconciled_ids": {
          "server": "4eaf9d7d67d0270ff37942597dc6a29c8611157be63af18a154b686fcb0ff82a"
        },
        "role_absence": {
          "consumer": true,
          "server": true
        }
      },
      "observed_ids": {
        "consumer": "9d38670423af58fb7c7533cad6ff62d84cc037a74b11abbc7f88c5fe9ae2743d",
        "server": "4eaf9d7d67d0270ff37942597dc6a29c8611157be63af18a154b686fcb0ff82a"
      },
      "ownership_sha256": "a30f685ce1f7b6c8f871caaea219fccf06a62b15c31ded3a4579bf2f8bf8baf4"
    }
  ]
}

The test's separate observer proved both full IDs and both exact label sets absent before data removal. A second independent readback at 23:28:22 UTC, using another fresh empty Docker configuration, confirmed both exact missing-object responses and empty role-label listings. The owned fixture directory is absent, and the source worktree is clean at the reviewed head. Docker events independently show only create/destroy for the reconciled server-labelled object; the consumer has create/start/kill/die/destroy. Thus no recovered object was started.

The prior c34e0b21 lost-create predecessor remains one expected inert failure / zero setup errors, with no effectful run. The separate de6 mount NONPASS and accepted 858 generated-mount receipt remain distinct.

The independent Security counter-review is policy-blocked and set aside. No independent receipt acceptance or broader qualification is claimed, and this review step will not be retried or replaced. No native Server, SDK/package/application, API, Worker, production credential or vault operation ran. Dependency/profile/caller/current-CI/package/installed/shared #216 gates remain held, and no #419 operation was attempted. Sanitized diagnostics retain the existing seven-day limit.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security's current counter-review step for the synthetic cleanup receipt is policy-blocked and is set aside; no receipt or broader qualification is claimed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:sdk-php Work done by the sdk-php repo agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant