From a162ae57f0ade3ca5c093518d2832a63449724ea Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Thu, 18 Jun 2026 13:23:17 -0400 Subject: [PATCH] Strengthen package artifact examples --- docs/registry-and-packages.md | 51 ++++++++++++++ .../class-wp-agent-package-artifact-type.php | 66 +++++++++++++++++++ tests/no-product-imports-smoke.php | 21 ++++++ tests/package-lifecycle-smoke.php | 32 +++++++++ 4 files changed, 170 insertions(+) diff --git a/docs/registry-and-packages.md b/docs/registry-and-packages.md index cfdd6dc..991633b 100644 --- a/docs/registry-and-packages.md +++ b/docs/registry-and-packages.md @@ -134,6 +134,31 @@ Normalized package fields: Construct from a manifest with `WP_Agent_Package::from_array( $manifest )`. +Example package artifacts should use neutral package-relative names. A package can describe workspace context that a host may preload later without Agents API knowing how that host stores, renders, or materializes it: + +```php +$package = WP_Agent_Package::from_array( + array( + 'slug' => 'example-assistant-package', + 'version' => '1.2.3', + 'agent' => array( + 'slug' => 'example-assistant', + 'label' => 'Example Assistant', + ), + 'artifacts' => array( + array( + 'type' => 'example/context', + 'slug' => 'workspace-context', + 'label' => 'Workspace Context', + 'description' => 'Context payload available to host-owned workspace preload flows.', + 'source' => 'artifacts/workspace/context.md', + 'requires' => array( 'workspace.context' ), + ), + ), + ) +); +``` + ## Package artifacts `WP_Agent_Package_Artifact` records identity and payload location only. Consumers own interpretation, install/update behavior, and review UI. @@ -153,6 +178,32 @@ Core fields: Package artifacts can describe diff callbacks through registered artifact types, which supports human-reviewable adoption/update flows without tying package review to live runtime pending-action approval. +Artifact type registrations can include `example_artifacts` so hosts, docs, and validators can publish canonical examples for a type. Examples are normalized through `WP_Agent_Package_Artifact`, must use the registered type, and follow the same package-relative source validation as real package artifacts: + +```php +add_action( + 'wp_agent_package_artifacts_init', + static function (): void { + wp_register_agent_package_artifact_type( + 'example/context', + array( + 'label' => 'Context Artifact', + 'description' => 'Package-relative context payload consumed by host-owned materializers.', + 'example_artifacts' => array( + array( + 'slug' => 'workspace-context', + 'source' => 'artifacts/workspace/context.md', + 'requires' => array( 'workspace.context' ), + ), + ), + ) + ); + } +); +``` + +Agents API does not implement a workspace preload primitive. It only preserves package artifact identity, source, requirements, and registered type metadata so a host adapter can decide whether to preload, skip, diff, approve, or materialize the artifact. + ## Package lifecycle primitives Agents API owns storage-neutral primitives for package update planning. They do not create database rows, read package files from disk, approve changes, or apply artifacts. Consumers provide installed/current/target artifact arrays and decide how to store or display the resulting plan. diff --git a/src/Packages/class-wp-agent-package-artifact-type.php b/src/Packages/class-wp-agent-package-artifact-type.php index 5e03e02..9d9c5a4 100644 --- a/src/Packages/class-wp-agent-package-artifact-type.php +++ b/src/Packages/class-wp-agent-package-artifact-type.php @@ -69,6 +69,13 @@ final class WP_Agent_Package_Artifact_Type { */ private array $meta = array(); + /** + * Example artifact declarations for documentation and host validation. + * + * @var array> + */ + private array $example_artifacts = array(); + /** * Constructor. * @@ -104,6 +111,14 @@ public function __construct( string $type, array $args = array() ) { $this->meta = self::prepare_meta( $args['meta'] ); } + + if ( isset( $args['example_artifacts'] ) ) { + if ( ! is_array( $args['example_artifacts'] ) ) { + throw new InvalidArgumentException( 'Agent package artifact type example_artifacts property must be an array.' ); + } + + $this->example_artifacts = $this->prepare_example_artifacts( $args['example_artifacts'] ); + } } /** @@ -193,6 +208,15 @@ public function get_meta(): array { return $this->meta; } + /** + * Retrieves example artifact declarations. + * + * @return array> + */ + public function get_example_artifacts(): array { + return $this->example_artifacts; + } + /** * Exports registration arguments. * @@ -207,8 +231,50 @@ public function to_array(): array { 'diff_callback' => $this->diff_callback, 'import_callback' => $this->import_callback, 'delete_callback' => $this->delete_callback, + 'example_artifacts' => $this->example_artifacts, 'meta' => $this->meta, ); } + + /** + * Normalizes example artifact declarations against the generic artifact shape. + * + * @param array $examples Raw example declarations. + * @return array> + */ + private function prepare_example_artifacts( array $examples ): array { + $prepared = array(); + foreach ( $examples as $example ) { + if ( ! is_array( $example ) ) { + throw new InvalidArgumentException( 'Agent package artifact type example_artifacts entries must be arrays.' ); + } + + $artifact = WP_Agent_Package_Artifact::from_array( + array_merge( + $this->string_keyed_array( $example ), + array( 'type' => $this->type ) + ) + ); + + $prepared[] = $artifact->to_array(); + } + + return $prepared; + } + + /** + * @param array $values Raw values. + * @return array + */ + private function string_keyed_array( array $values ): array { + $prepared = array(); + foreach ( $values as $key => $value ) { + if ( is_string( $key ) ) { + $prepared[ $key ] = $value; + } + } + + return $prepared; + } } } diff --git a/tests/no-product-imports-smoke.php b/tests/no-product-imports-smoke.php index 96d5917..249d468 100644 --- a/tests/no-product-imports-smoke.php +++ b/tests/no-product-imports-smoke.php @@ -22,6 +22,11 @@ 'src' => $agents_api_dir . '/src', ); +$package_example_paths = array( + 'README.md' => $agents_api_dir . '/README.md', + 'docs/registry-and-packages.md' => $agents_api_dir . '/docs/registry-and-packages.md', +); + $forbidden_namespaces = array( 'ExampleProduct\\Core\\Steps', 'ExampleProduct\\Core\\Database\\Jobs', @@ -146,6 +151,22 @@ } agents_api_smoke_assert_equals( array(), $matches, 'agents-api production source has no product imports, product vocabulary, admin UI registrations, or table ownership', $failures, $passes ); + +$package_example_matches = array(); +foreach ( $package_example_paths as $relative_path => $path ) { + if ( ! is_file( $path ) ) { + continue; + } + + $source = (string) file_get_contents( $path ); + foreach ( $forbidden_product_vocabulary as $term ) { + if ( preg_match( '/(? array( + array( + 'slug' => 'workspace-context', + 'label' => 'Workspace Context', + 'description' => 'Package-relative context artifact for a host-owned workspace preload flow.', + 'source' => 'artifacts/workspace/context.md', + 'requires' => array( 'workspace.context' ), + ), + ), 'validate_callback' => static function ( WP_Agent_Package_Artifact $artifact, array $context ): array { return array( $artifact->get_slug(), $context['phase'] ?? '' ); }, @@ -206,4 +215,27 @@ static function (): void { $callback_result = WP_Agent_Package_Artifact_Callbacks::validate( $artifact, array( 'phase' => 'install' ) ); agents_api_smoke_assert_equals( array( 'welcome', 'install' ), $callback_result, 'validate callback receives artifact and context', $failures, $passes ); +$registered_type = wp_get_agent_package_artifact_type( 'example/prompt' ); +$example_artifacts = null === $registered_type ? array() : $registered_type->get_example_artifacts(); +agents_api_smoke_assert_equals( 'artifacts/workspace/context.md', $example_artifacts[0]['source'] ?? '', 'artifact type examples normalize package-relative workspace context sources', $failures, $passes ); +agents_api_smoke_assert_equals( array( 'workspace.context' ), $example_artifacts[0]['requires'] ?? array(), 'artifact type examples normalize neutral workspace requirements', $failures, $passes ); + +$invalid_example_rejected = false; +try { + new WP_Agent_Package_Artifact_Type( + 'example/context', + array( + 'example_artifacts' => array( + array( + 'slug' => 'escape-package-root', + 'source' => '../outside.md', + ), + ), + ) + ); +} catch ( InvalidArgumentException $e ) { + $invalid_example_rejected = true; +} +agents_api_smoke_assert_equals( true, $invalid_example_rejected, 'artifact type examples reject package-escaping sources', $failures, $passes ); + agents_api_smoke_finish( 'Agents API package lifecycle', $failures, $passes );