signature = $signature; } /** * @param int $now * @return array{verdict:string,name:string,confidence:string,detail:string} */ public function classify( AgentStamp_Request $request, $now ) { $signed = $this->signature->check( $request, $now ); if ( 'valid' === $signed['status'] ) { $detail = $signed['detail']; $claimed = self::claimed_name( $request->user_agent ); if ( '' !== $claimed && $claimed !== $signed['name'] ) { $detail .= ' User-agent claimed ' . $claimed . '.'; } return array( 'verdict' => 'agent', 'name' => $signed['name'], 'confidence' => 'signed', 'detail' => $detail, ); } if ( 'invalid' === $signed['status'] ) { $claimed = self::claimed_name( $request->user_agent ); $detail = $signed['detail']; if ( '' !== $claimed ) { $detail .= ' User-agent claimed ' . $claimed . ', which was not treated as proof.'; } return array( 'verdict' => 'unknown', 'name' => '', 'confidence' => 'none', 'detail' => $detail, ); } $ai = AgentStamp_Agents::match_ai_ua( $request->user_agent ); if ( '' !== $ai ) { return array( 'verdict' => 'agent', 'name' => $ai, 'confidence' => 'declared', 'detail' => 'User-agent matches ' . $ai . '. The request was not signed.', ); } $crawler = AgentStamp_Agents::match_ai_crawler( $request->user_agent ); if ( '' !== $crawler ) { return array( 'verdict' => 'unknown', 'name' => '', 'confidence' => 'none', 'detail' => 'User-agent names ' . $crawler . ', a crawler. Crawlers do not check out, so the name was not used.', ); } if ( AgentStamp_Agents::is_other_bot( $request->user_agent ) ) { return array( 'verdict' => 'unknown', 'name' => '', 'confidence' => 'none', 'detail' => 'A crawler user-agent that is not a named shopping agent.', ); } if ( AgentStamp_Agents::is_human_browser( $request->user_agent ) ) { return array( 'verdict' => 'human', 'name' => '', 'confidence' => 'browser', 'detail' => 'Ordinary browser user-agent, no agent signature.', ); } $detail = '' === trim( $request->user_agent ) ? 'No user-agent on the checkout request.' : 'User-agent is not a browser and not a published AI agent.'; return array( 'verdict' => 'unknown', 'name' => '', 'confidence' => 'none', 'detail' => $detail, ); } /** * An order source that is an API client or a commerce protocol, not the checkout page. * * @param string $via WooCommerce created_via. * @return bool */ public static function is_api_channel( $via ) { $via = (string) $via; if ( 'rest-api' === $via ) { return true; } return 1 === preg_match( '/(?:^|[-_])(?:ucp|acp|ap2|mcp|agent|agentic|chatgpt|openai|gemini|copilot|perplexity)(?:$|[-_])/i', $via ); } /** * Orders that did not come through the store's checkout page. * * A signed or declared agent keeps its name. Anything else is API, named by * the client's user-agent product, or by the channel when there is none. * * @param array{verdict:string,name:string,confidence:string,detail:string} $result * @param string $user_agent * @param string $via WooCommerce created_via. * @return array{verdict:string,name:string,confidence:string,detail:string} */ public static function for_channel( array $result, $user_agent, $via ) { $via = (string) $via; if ( in_array( $via, array( '', 'checkout', 'store-api', 'admin' ), true ) ) { return $result; } if ( 'agent' === $result['verdict'] ) { $result['detail'] .= ' Created through ' . $via . ', not the checkout page.'; return $result; } if ( 'unknown' === $result['verdict'] && false !== stripos( $result['detail'], 'Signature' ) ) { return $result; } $client = ''; $first = strtok( trim( (string) $user_agent ), ' ' ); if ( false !== $first && 0 !== strpos( $first, 'Mozilla/' ) && preg_match( '/^([A-Za-z][A-Za-z0-9._-]{0,40})(?:\/|$)/', $first, $match ) ) { $client = $match[1]; } $name = '' !== $client ? $client : $via; return array( 'verdict' => 'api', 'name' => $name, 'confidence' => 'channel', 'detail' => 'Created through ' . $via . ', not the checkout page. ' . ( '' !== $client ? 'The client called itself ' . $client . '.' : 'The client gave no product name.' ) . ' No agent signature.', ); } /** * @param string $user_agent * @return string */ private static function claimed_name( $user_agent ) { $claimed = AgentStamp_Agents::match_ai_ua( $user_agent ); return '' !== $claimed ? $claimed : AgentStamp_Agents::match_ai_crawler( $user_agent ); } /** * Short label for the order list. * * @param array{verdict:string,name:string,confidence:string,detail:string} $result * @return string */ public static function label( array $result ) { if ( 'human' === $result['verdict'] ) { return 'Human'; } if ( 'agent' === $result['verdict'] ) { $how = ( 'signed' === $result['confidence'] ) ? 'signed' : ( 'payment' === $result['confidence'] ? 'payment' : 'declared' ); return $result['name'] . ' (' . $how . ')'; } if ( 'unattended' === $result['verdict'] ) { return 'Unattended'; } if ( 'likely' === $result['verdict'] ) { return 'Likely agent'; } if ( 'api' === $result['verdict'] ) { return 'API (' . $result['name'] . ')'; } return 'Unknown'; } }