From 12d125e38f4a682065f3bb8af87a8619146877e3 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Thu, 6 Aug 2026 17:36:01 -0300 Subject: [PATCH 1/2] Security: surface partial package adoption as a non-success status WP_Agent_Package_Adoption_Result::canonical_status() mapped a 'partial' adoption result to STATUS_SUCCEEDED. The orchestrator emits 'partial' precisely when some artifacts were applied but OTHERS FAILED. As a result the canonical run-result envelope reported top-level status = succeeded for a run that had failures. The failures survived only inside outputs.failed, invisible to any consumer keying on the envelope status, so partial adoptions with real failures silently read as fully successful. Map 'partial' to STATUS_INCOMPLETE, an existing non-success canonical status that accurately describes a run that only partially completed. Fully successful adoptions still map to SUCCEEDED and fully failed ones to FAILED. Adds tests/package-adoption-result-canonical-status-smoke.php asserting a partial result yields a non-succeeded canonical status with the failed artifacts still visible, plus no-regression coverage on the clean success and full-failure paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- composer.json | 1 + ...class-wp-agent-package-adoption-result.php | 3 + ...adoption-result-canonical-status-smoke.php | 77 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 tests/package-adoption-result-canonical-status-smoke.php diff --git a/composer.json b/composer.json index 8b864bf..e624741 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "php tests/package-duplicate-artifact-identity-smoke.php", "php tests/package-capability-contract-smoke.php", "php tests/package-adoption-orchestration-smoke.php", + "php tests/package-adoption-result-canonical-status-smoke.php", "php tests/execution-principal-smoke.php", "php tests/effective-agent-resolver-smoke.php", "php tests/runtime-profile-smoke.php", diff --git a/src/Packages/class-wp-agent-package-adoption-result.php b/src/Packages/class-wp-agent-package-adoption-result.php index 5ed4d2d..fcba06d 100644 --- a/src/Packages/class-wp-agent-package-adoption-result.php +++ b/src/Packages/class-wp-agent-package-adoption-result.php @@ -208,6 +208,9 @@ private function canonical_status(): string { if ( 'failed' === $this->status ) { return \AgentsAPI\AI\WP_Agent_Run_Result_Envelope::STATUS_FAILED; } + if ( 'partial' === $this->status ) { + return \AgentsAPI\AI\WP_Agent_Run_Result_Envelope::STATUS_INCOMPLETE; + } if ( in_array( $this->status, array( 'skipped', 'needs-approval' ), true ) ) { return 'needs-approval' === $this->status ? \AgentsAPI\AI\WP_Agent_Run_Result_Envelope::STATUS_APPROVAL_REQUIRED : \AgentsAPI\AI\WP_Agent_Run_Result_Envelope::STATUS_SKIPPED; } diff --git a/tests/package-adoption-result-canonical-status-smoke.php b/tests/package-adoption-result-canonical-status-smoke.php new file mode 100644 index 0000000..067ef4d --- /dev/null +++ b/tests/package-adoption-result-canonical-status-smoke.php @@ -0,0 +1,77 @@ + 'example/prompt', 'artifact_id' => 'applied-one', 'apply_status' => 'applied' ); +$failed_artifact = array( 'artifact_type' => 'example/prompt', 'artifact_id' => 'failed-one', 'apply_status' => 'failed', 'apply_reason' => 'import rejected' ); + +echo "\n[1] Partial adoption surfaces a non-success canonical status with failures intact:\n"; + +$partial_result = new WP_Agent_Package_Adoption_Result( + 'partial', + 'demo-agent', + array(), + null, + array( $applied_artifact ), + array(), + array( $failed_artifact ) +); +$partial_envelope = $partial_result->to_run_result_envelope(); +$partial_status = $partial_envelope->get_status(); +$partial_outputs = $partial_envelope->get_outputs(); + +agents_api_smoke_assert_equals( false, WP_Agent_Run_Result_Envelope::STATUS_SUCCEEDED === $partial_status, 'partial adoption does not report succeeded', $failures, $passes ); +agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_INCOMPLETE, $partial_status, 'partial adoption maps to the incomplete canonical status', $failures, $passes ); +agents_api_smoke_assert_equals( 1, count( $partial_outputs['failed'] ?? array() ), 'failed artifacts remain visible in the envelope outputs', $failures, $passes ); +agents_api_smoke_assert_equals( 'failed-one', $partial_outputs['failed'][0]['artifact_id'] ?? null, 'failed artifact identity is preserved', $failures, $passes ); + +echo "\n[2] Fully successful adoption still maps to succeeded (no regression):\n"; + +$success_result = new WP_Agent_Package_Adoption_Result( + 'applied', + 'demo-agent', + array(), + null, + array( $applied_artifact ) +); +$success_status = $success_result->to_run_result_envelope()->get_status(); +agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_SUCCEEDED, $success_status, 'clean adoption maps to succeeded', $failures, $passes ); + +echo "\n[3] Fully failed adoption still maps to failed (no regression):\n"; + +$failed_result = new WP_Agent_Package_Adoption_Result( + 'failed', + 'demo-agent', + array(), + null, + array(), + array(), + array( $failed_artifact ) +); +$failed_status = $failed_result->to_run_result_envelope()->get_status(); +agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_FAILED, $failed_status, 'fully failed adoption maps to failed', $failures, $passes ); + +agents_api_smoke_finish( 'Agents API package adoption result canonical status', $failures, $passes ); From a8e3e961c2042cfadea285eb3bcd2d4f7018268d Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 26 Aug 2026 21:51:47 +0000 Subject: [PATCH 2/2] fix: distinguish benign partial package adoption --- ...class-wp-agent-package-adoption-result.php | 2 +- ...adoption-result-canonical-status-smoke.php | 40 ++++++++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Packages/class-wp-agent-package-adoption-result.php b/src/Packages/class-wp-agent-package-adoption-result.php index fcba06d..1a6e7e4 100644 --- a/src/Packages/class-wp-agent-package-adoption-result.php +++ b/src/Packages/class-wp-agent-package-adoption-result.php @@ -208,7 +208,7 @@ private function canonical_status(): string { if ( 'failed' === $this->status ) { return \AgentsAPI\AI\WP_Agent_Run_Result_Envelope::STATUS_FAILED; } - if ( 'partial' === $this->status ) { + if ( 'partial' === $this->status && ! empty( $this->failed_artifacts ) ) { return \AgentsAPI\AI\WP_Agent_Run_Result_Envelope::STATUS_INCOMPLETE; } if ( in_array( $this->status, array( 'skipped', 'needs-approval' ), true ) ) { diff --git a/tests/package-adoption-result-canonical-status-smoke.php b/tests/package-adoption-result-canonical-status-smoke.php index 067ef4d..052554d 100644 --- a/tests/package-adoption-result-canonical-status-smoke.php +++ b/tests/package-adoption-result-canonical-status-smoke.php @@ -2,9 +2,8 @@ /** * Pure-PHP smoke test for package adoption canonical status mapping. * - * Guards against partial adoptions (some artifacts applied, others failed) - * being surfaced as a top-level `succeeded` canonical status, which would - * hide the failures from any consumer keying on the envelope status. + * Guards the canonical distinction between partial adoptions with failures + * and partial adoptions containing only intentionally skipped artifacts. * * Run with: php tests/package-adoption-result-canonical-status-smoke.php * @@ -26,9 +25,10 @@ use AgentsAPI\AI\WP_Agent_Run_Result_Envelope; $applied_artifact = array( 'artifact_type' => 'example/prompt', 'artifact_id' => 'applied-one', 'apply_status' => 'applied' ); +$skipped_artifact = array( 'artifact_type' => 'example/prompt', 'artifact_id' => 'skipped-one', 'apply_status' => 'skipped', 'apply_reason' => 'not approved' ); $failed_artifact = array( 'artifact_type' => 'example/prompt', 'artifact_id' => 'failed-one', 'apply_status' => 'failed', 'apply_reason' => 'import rejected' ); -echo "\n[1] Partial adoption surfaces a non-success canonical status with failures intact:\n"; +echo "\n[1] Partial adoption with a failure maps to incomplete:\n"; $partial_result = new WP_Agent_Package_Adoption_Result( 'partial', @@ -39,16 +39,36 @@ array(), array( $failed_artifact ) ); -$partial_envelope = $partial_result->to_run_result_envelope(); +$partial_envelope = $partial_result->to_canonical_envelope(); $partial_status = $partial_envelope->get_status(); $partial_outputs = $partial_envelope->get_outputs(); -agents_api_smoke_assert_equals( false, WP_Agent_Run_Result_Envelope::STATUS_SUCCEEDED === $partial_status, 'partial adoption does not report succeeded', $failures, $passes ); agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_INCOMPLETE, $partial_status, 'partial adoption maps to the incomplete canonical status', $failures, $passes ); agents_api_smoke_assert_equals( 1, count( $partial_outputs['failed'] ?? array() ), 'failed artifacts remain visible in the envelope outputs', $failures, $passes ); agents_api_smoke_assert_equals( 'failed-one', $partial_outputs['failed'][0]['artifact_id'] ?? null, 'failed artifact identity is preserved', $failures, $passes ); +agents_api_smoke_assert_equals( 'partial', $partial_result->get_status(), 'raw partial status remains unchanged', $failures, $passes ); +agents_api_smoke_assert_equals( 'partial', $partial_result->to_array()['status'], 'serialized partial status remains unchanged', $failures, $passes ); -echo "\n[2] Fully successful adoption still maps to succeeded (no regression):\n"; +echo "\n[2] Partial adoption with only an intentional skip maps to succeeded:\n"; + +$skipped_result = new WP_Agent_Package_Adoption_Result( + 'partial', + 'demo-agent', + array(), + null, + array( $applied_artifact ), + array( $skipped_artifact ) +); +$skipped_envelope = $skipped_result->to_canonical_envelope(); +$skipped_outputs = $skipped_envelope->get_outputs(); + +agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_SUCCEEDED, $skipped_envelope->get_status(), 'benign partial adoption maps to succeeded', $failures, $passes ); +agents_api_smoke_assert_equals( 0, count( $skipped_outputs['failed'] ?? array() ), 'benign partial adoption has no failed artifacts', $failures, $passes ); +agents_api_smoke_assert_equals( 'skipped-one', $skipped_outputs['skipped'][0]['artifact_id'] ?? null, 'skipped artifact remains visible in the envelope outputs', $failures, $passes ); +agents_api_smoke_assert_equals( 'partial', $skipped_result->get_status(), 'raw benign partial status remains unchanged', $failures, $passes ); +agents_api_smoke_assert_equals( 'partial', $skipped_result->to_array()['status'], 'serialized benign partial status remains unchanged', $failures, $passes ); + +echo "\n[3] Fully successful adoption maps to succeeded:\n"; $success_result = new WP_Agent_Package_Adoption_Result( 'applied', @@ -57,10 +77,10 @@ null, array( $applied_artifact ) ); -$success_status = $success_result->to_run_result_envelope()->get_status(); +$success_status = $success_result->to_canonical_envelope()->get_status(); agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_SUCCEEDED, $success_status, 'clean adoption maps to succeeded', $failures, $passes ); -echo "\n[3] Fully failed adoption still maps to failed (no regression):\n"; +echo "\n[4] Fully failed adoption maps to failed:\n"; $failed_result = new WP_Agent_Package_Adoption_Result( 'failed', @@ -71,7 +91,7 @@ array(), array( $failed_artifact ) ); -$failed_status = $failed_result->to_run_result_envelope()->get_status(); +$failed_status = $failed_result->to_canonical_envelope()->get_status(); agents_api_smoke_assert_equals( WP_Agent_Run_Result_Envelope::STATUS_FAILED, $failed_status, 'fully failed adoption maps to failed', $failures, $passes ); agents_api_smoke_finish( 'Agents API package adoption result canonical status', $failures, $passes );