*/ public static function document(): array { $paths = array(); foreach ( AgentAdapt_Capabilities::all() as $id => $cap ) { $params = array(); $body = array(); foreach ( $cap['params'] as $name => $schema ) { $in = $schema['in'] ?? ( str_contains( (string) $cap['path'], '{' . $name . '}' ) ? 'path' : ( 'GET' === $cap['method'] || 'DELETE' === $cap['method'] ? 'query' : 'body' ) ); $clean = self::schema( $schema ); if ( 'body' === $in ) { $body[ $name ] = $clean; continue; } $params[] = array_filter( array( 'name' => $name, 'in' => $in, 'required' => 'path' === $in || ! empty( $schema['required'] ), 'description' => $schema['description'] ?? null, 'schema' => $clean, ), static fn( $v ) => null !== $v ); } $op = array( 'operationId' => str_replace( '.', '_', $id ), 'summary' => (string) ( $cap['summary'] ?? $id ), 'description' => (string) ( $cap['description'] ?? '' ), 'security' => array( array( 'dpop' => array() ) ), // Account scopes the call needs; empty means a signed-out Session may call it. 'x-agentadapt-scopes' => $cap['scopes'], 'responses' => array( (string) ( $cap['status'] ?? 200 ) => array( 'description' => 'Success', 'content' => array( 'application/json' => array( 'schema' => $cap['response'] ?? array( 'type' => 'object' ) ) ), ), '401' => array( 'description' => 'invalid_token or invalid_dpop_proof (WWW-Authenticate: DPoP)' ), '403' => array( 'description' => 'sign_in_required or insufficient_scope (WWW-Authenticate: DPoP)' ), '429' => array( 'description' => 'rate_limited (Retry-After)' ), ), ); // Refusals particular to this capability, as status => description. foreach ( (array) ( $cap['errors'] ?? array() ) as $status => $why ) { $op['responses'][ (string) $status ] = array( 'description' => (string) $why ); } if ( ! empty( $cap['proposes_operation'] ) ) { $op['responses']['202'] = array( 'description' => 'An operation was proposed instead of performed (Personal Agent Protocol operations extension, for agents that list it). Nothing has happened until it is confirmed at the operations endpoint.', 'content' => array( 'application/json' => array( 'schema' => array( 'type' => 'object', 'properties' => array( 'operation' => array( 'type' => 'object' ) ), 'required' => array( 'operation' ) ) ) ), ); $op['x-poppy-operation'] = true; } if ( $params ) { $op['parameters'] = $params; } if ( $body ) { $required = array_keys( array_filter( $cap['params'], static fn( $s ) => ! empty( $s['required'] ) && ( $s['in'] ?? 'body' ) === 'body' ) ); $op['requestBody'] = array( 'required' => (bool) $required, 'content' => array( 'application/json' => array( 'schema' => array_filter( array( 'type' => 'object', 'properties' => $body, 'required' => $required ?: null ) ), ), ), ); } $paths[ (string) $cap['path'] ][ strtolower( $cap['method'] ) ] = $op; } ksort( $paths ); return array( 'openapi' => '3.1.0', 'info' => array( /* translators: %s: store name */ 'title' => sprintf( __( '%s — agent API', 'agent-adapt' ), wp_specialchars_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES ) ), 'version' => AGENTADAPT_VERSION, 'description' => __( 'For personal agents using the Personal Agent Protocol. Every call needs a DPoP-bound Session Token from this store\'s token endpoint (see /.well-known/poppy.json).', 'agent-adapt' ), ), 'servers' => array( array( 'url' => untrailingslashit( rest_url( AgentAdapt_Rest::NS ) ) ) ), 'paths' => $paths, 'components' => array( 'securitySchemes' => array( // RFC 9449 registers the DPoP HTTP authentication scheme. 'dpop' => array( 'type' => 'http', 'scheme' => 'DPoP' ), ), ), ); } /** * A param definition minus our own keys. * * @param array $schema Param definition. * @return array */ private static function schema( array $schema ): array { unset( $schema['required'], $schema['in'], $schema['description'] ); return $schema; } }